# -*- coding: utf-8 -*-
"""Python code for controlling the Lin Engineering R356 Motor Controller"""
from __future__ import absolute_import, print_function, division
import contextlib
import glob
import logging
import math
import sys
import time
from .import commands
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
[docs]def decode_motor_response(resp):
"""Returns status as a string"""
status_byte = resp[resp.find(b'/0') + 2]
err_code = status_byte & 0x0f
# descriptions from page 11 of Silverpak23C_R356_Commands.pdf
descriptions = {
0: "No Error",
1: "Initialization error",
2: "Bad command",
3: "Bad operand",
5: "Communication error (internal communication error)",
7: "Not initialized (controller not initialized before attempting "
"a move",
9: "Overload error (system could not keep up with commanded position",
11: "Move not allowed",
15: "Command overflow (Unit was busy executing another command",
}
return descriptions[err_code]
[docs]def fake_spin(fake, times=80, sleep=0.1):
fake.revolve()
for _ in range(times):
print(fake.get_position())
time.sleep(sleep)
[docs]def compare(fake, real, times=80, sleep=0.1):
real.zero()
fake.zero()
time.sleep(0.3)
real.revolve()
fake.revolve()
time.sleep(0.2)
for _ in range(times):
print(real.get_position() - fake.get_position())
time.sleep(sleep)
[docs]class R356(object):
"""An R356 motor controller hooked up by USB.
Default settings (from user manual)
======================== =======================================
Function Description
======================== =======================================
Running Current (m) 25% of 3.0 Amps (0.75 Amps)
Holding Current (h) 10% of the running current
Step Resolution (j) 256x
Top Velocity (v) 305175 pps (microsteps / sec)
Acceleration (L) L = 1000, 6103500 usteps/sec^2
Position 0
Microstep Smoothness (o) 1500
Outputs (J) Both are turned off, J0, Outputs 1 & 2
Baud Rate 9600 bps
======================== =======================================
"""
MICROSTEPS = 256
# The number of steps that go by for one revolution of the motor.
MOTOR_STEPS_PER_REVOLUTION = 200
# encoder "clicks per revolution"
CPR = 2000
# reduction from the motor spinning to the radar spinning
REDUCTION = 40
# Max current percentage that should go to the motor
CURRENT = 40
TICKS_PER_REVOLUTION = CPR * 4
# multiply acceleration by this to get microsteps per second
ACCELERATION_RATIO = 5850
ACCELERATION = 25
DEGREES_PER_SECOND = 60
def __init__(self, address=1, dev=None, baudrate=9600):
"""
address is a number: usually 1.
"""
if dev is None:
devices = glob.glob('/dev/ttyUSB?')
if devices:
dev = devices[0]
else:
dev = '/dev/ttyUSB0'
import serial
self.address = address
self.name = dev
self._baudrate = baudrate
self.start = commands.start(self.address)
self.serial = serial.Serial(dev, baudrate, timeout=0.25, write_timeout=0.25)
# self.serial = serial.Serial(dev, baudrate)
self.program = []
self._current = None
self._encoder_ratio = None
self._acceleration = None
self._fake = FakeR356()
self.init_settings()
self._velocity = self.get_slew_speed()
self._fake.zero()
@property
def one_revolution_per_second(self):
"""Returns the command to velocity required for doing one revolution
per second"""
one_rps = (self.MOTOR_STEPS_PER_REVOLUTION
* self._microsteps
* self.REDUCTION)
return one_rps
def _rpm_to_velocity(self, desired_rpm):
"""Given a desired rpm, return the integer required to give to velocity
command to achieve the rpm velocity.
"""
one_rps = self.one_revolution_per_second
velocity = int(one_rps * desired_rpm / 60)
return velocity
def _degrees_per_sec_to_velocity(self, dps):
one_rps = self.one_revolution_per_second
velocity = one_rps * dps / 360
return velocity
[docs] def get_acceleration(self):
return self._acceleration
[docs] def get_requested_velocity(self):
return self._velocity
[docs] def get_degrees_per_second(self):
v = self.get_slew_speed()
self._velocity = v
dps = self._velocity_to_degrees_per_sec(v)
return dps
@property
def max_velocity(self):
"""The highest allowed velocity. Anything higher could cause problems."""
one_rps = self.one_revolution_per_second
return one_rps * 10
[docs] def spin(self):
return self.positive(0)
[docs] def revolve(self, times=1):
return self.positive(self.CPR * 4 * times)
[docs] def pnew_program(self):
"""
This is the first function that must be called when starting a new
program.
"""
self.program = []
command = commands.start(self.address)
self.program.append(command)
[docs] def run_current_program(self):
self._run_program()
return self.write_then_read(b''.join(self.program))
##############################
### Homing and Positioning ###
##############################
[docs] def pgo_home(self, num_steps=2000000):
self.program.append(commands.home_and_initialize(num_steps))
[docs] def go_home(self, num_steps=2000000):
return self.do(commands.home_and_initialize(num_steps))
[docs] def phome(self, num_steps=2000000):
v = int(round(self.get_degrees_per_second()))
self.pdegrees_per_second(20)
self.pgo_home(num_steps)
self.pdegrees_per_second(v)
# if we let the position stay zero, then the motor cannot move
# negatively at all. We need the position to read as a multiple of the
# ticks per revolution so that we can move negatively.
self.pset_position(self.TICKS_PER_REVOLUTION * 10)
self._fake.zero()
self._fake.degrees_per_second(v)
[docs] def home(self, num_steps=2000000):
self.pnew_program()
self.phome(num_steps)
self.run_current_program()
self._fake._reset()
[docs] def pzero(self):
"""Sets current position to 0"""
self.pset_position(0)
[docs] def zero(self):
"""Sets current position to 0"""
self.set_position(0)
[docs] def pset_position(self, pos):
self.program.append(commands.set_current_position(pos))
self._fake._initial_position = pos
[docs] def set_position(self, pos):
ret = self.do(commands.set_current_position(pos))
self._fake.set_position(pos)
return ret
[docs] def pmove_to(self, pos):
self.program.append(commands.move_to(pos))
[docs] def move_to(self, position):
ret = self.do(commands.move_to(position))
self._fake.move_to(position)
return ret
[docs] def phome_sensor_polarity(self, p):
self.program.append(commands.set_home_sensor_polarity(p))
[docs] def home_sensor_polarity(self, p):
return self.do(commands.set_home_sensor_polarity(p))
[docs] def ppositive(self, steps):
self.program.append(commands.move_relative_positive(steps))
[docs] def positive(self, steps):
ret = self.do(commands.move_relative_positive(steps))
self._fake.positive(steps)
return ret
[docs] def pnegative(self, steps):
self.program.append(commands.move_relative_negative(steps))
[docs] def negative(self, steps):
if steps < 0:
steps = abs(steps)
ret = self.do(commands.move_relative_negative(steps))
self._fake.negative(steps)
return ret
[docs] def pjog_distance(self, distance):
self.program.append(commands.set_jog_distance(distance))
[docs] def jog_distance(self, distance):
return self.do(commands.set_jog_distance(distance))
[docs] def pstop(self):
self.program.append(commands.terminate_current_command())
[docs] def stop(self):
ret = self.do(commands.terminate_current_command())
self._fake.stop()
return ret
# def preverse(self, direction):
# self.program.append(commands.reverse(direction))
# def reverse(self, direction):
# ret = self.do(commands.reverse(direction))
# self._fake.reverse(direction)
# return ret
#########################
### Velocity commands ###
#########################
[docs] def pvelocity(self, v):
self._velocity = v
self.program.append(commands.set_velocity(v))
[docs] def velocity(self, v):
"""Sets the velocity immediately."""
if v > self.max_velocity:
raise ValueError('It is dangerous to spin that fast.')
self._velocity = v
ret = self.do(commands.set_velocity(v))
self._fake.velocity(v)
return ret
[docs] def pacceleration(self, a):
self._acceleration = a
self.program.append(commands.set_acceleration(a))
[docs] def acceleration(self, a):
"""Sets the acceleration immediately."""
self._acceleration = a
ret = self.do(commands.set_acceleration(a))
self._fake.acceleration(a)
return ret
#######################
### Setting current ###
#######################
[docs] def pcurrent(self, current):
self._current = current
self.program.append(commands.set_current(current))
[docs] def current(self, current):
self._current = current
return self.do(commands.set_current(current))
[docs] def phold_current(self, current):
self._hold_current = current
self.program.append(commands.hold_current(current))
[docs] def hold_current(self, current):
self._hold_current = current
return self.do(commands.hold_current(current))
#############################
### Looping and Branching ###
#############################
[docs] @contextlib.contextmanager
def loop(self, repetitions):
self.program.append(commands.begin_loop())
yield
self.program.append(commands.end_loop(repetitions))
[docs] def pdelay(self, milliseconds):
self.program.append(commands.delay(milliseconds))
[docs] def phalt_wait(self, value, pin):
self.program.append(commands.halt_wait(value, pin))
[docs] def pskip_next_if(self, value, pin):
self.program.append(commands.skip_next_if(value, pin))
[docs] def pmode(self, mode):
self.program.append(commands.set_mode(mode))
[docs] def mode(self, mode):
return self.do(commands.set_mode(mode))
#################################################
### Position Correction - Encoder Option Only ###
#################################################
[docs] def pspecial_modes(self, mode):
self.program.append(commands.special_modes(mode))
[docs] def special_modes(self, mode):
return self.do(commands.special_modes(mode))
[docs] def pset_accuracy(self, a):
self._accuracy = a
self.program.append(commands.set_accuracy(a))
[docs] def set_accuracy(self, a):
self._accuracy = a
return self.do(commands.set_accuracy(a))
[docs] def pencoder_ratio(self, ratio):
self._encoder_ratio = ratio
self.program.append(commands.set_encoder_ratio(ratio))
[docs] def encoder_ratio(self, ratio):
self._encoder_ratio = ratio
return self.do(commands.set_encoder_ratio(ratio))
[docs] def poverload_timeout(self, timeout):
self.program.append(commands.set_overload_timeout(timeout))
[docs] def overload_timeout(self, timeout):
return self.do(commands.set_overload_timeout(timeout))
[docs] def precover_encoder(self):
self.program.append(commands.recover_encoder())
[docs] def recover_encoder(self):
return self.do(commands.recover_encoder())
##################################
### Program Storage and Recall ###
##################################
[docs] def pstore_program(self, program):
self.program.append(commands.store_program(program))
[docs] def pexecute_program(self, program):
self.program.append(commands.execute_program(program))
#########################
### Program Execution ###
#########################
def _run_program(self):
self.program.append(commands.run_program())
[docs] def prepeat_program(self):
self.program.append(commands.repeat_program())
[docs] def repeat_program(self):
return self.do(commands.repeat_program())
#####################
### Microstepping ###
#####################
[docs] def pmicrosteps_per_step(self, microsteps):
self._microsteps = microsteps
self.program.append(commands.set_microsteps_per_step(microsteps))
[docs] def microsteps_per_step(self, microsteps):
self._microsteps = microsteps
ret = self.do(commands.set_microsteps_per_step(microsteps))
self._fake.microsteps_per_step(microsteps)
return ret
[docs] def padjust_microsteps(self, adjustment):
self.program.append(commands.adjust_microsteps(adjustment))
[docs] def adjust_microsteps(self, adjustment):
ret = self.do(commands.adjust_microsteps(adjustment))
self._fake.adjust_microsteps(adjustment)
return ret
################################
### On/Off Drivers (Outputs) ###
################################
[docs] def pon_off_driver(self, value):
self.program.append(commands.on_off_driver(value))
[docs] def on_off_driver(self, value):
return self.do(commands.on_off_driver(value))
######################
### Query Commands ###
######################
def _get_actual_position(self):
actual = self.get_int(self.start + commands.get_position())
return actual
[docs] def get_actual_position(self):
actual = self._get_actual_position()
if self._fake._velocity is not None:
estimated = self._fake.get_position()
diff = actual - estimated
orig = self._fake._initial_position * 1000 / self._fake._encoder_ratio
self._fake.set_initial_position(orig + diff)
return actual
[docs] def get_position(self):
return self._fake.get_position()
[docs] def get_start_velocity(self):
return self.get_int(self.start + commands.get_start_velocity())
[docs] def get_slew_speed(self):
return self.get_int(self.start + commands.get_slew_speed())
[docs] def get_stop_speed(self):
return self.get_int(self.start + commands.get_stop_speed())
[docs] def get_velocity(self):
return self.get_int(self.start + commands.get_velocity())
[docs] def get_step_size(self):
return self.get_int(self.start + commands.get_step_size())
[docs] def get_o_value(self):
return self.get_int(self.start + commands.get_o_value())
[docs] def get_encoder_position(self):
return self.get_int(self.start + commands.get_encoder_position())
[docs] def perase_programs(self):
self.program.append(commands.erase_programs())
[docs] def erase_programs(self):
return self.do(self.start + commands.erase_programs())
[docs] def get_current_command(self):
return self.write_then_read(self.start + commands.get_current_command())
[docs] def get_firmware(self):
return self.write_then_read(self.start + commands.get_firmware())
[docs] def get_controller_status(self):
command = self.start + commands.get_controller_status()
return self.write_then_read(command)
[docs] def send_number(self, n):
return self.write_then_read(self.start + commands.send_number(n))
[docs] def pbaudrate(self, rate):
return self.program.append(commands.set_baud_rate(rate))
[docs] def baudrate(self, rate):
if rate not in (9600, 19200, 38400):
raise ValueError(('baudrate "%d" not allowed: must be one of 9600, '
'19200, or 38400.') % rate)
self._baudrate = rate
return self.do(commands.set_baud_rate(rate))
### Functions for sending and receiving data to the controller
[docs] def programmify(self, command):
"""
Turn the specified command into a program by prepending the "start"
command and appending the "run" command. This does not actually run
the program, just returns bytes that are ready to run.
"""
return self.start + command + commands.run_program()
[docs] def write(self, data, append_newline=True):
"""Write data to the motor controller"""
logger.debug(data.decode())
if append_newline:
data += b'\r\n'
self.serial.write(data)
[docs] def write_then_read(self, data):
# We can clear the input data buffer since we just want the newest thing
self.serial.reset_input_buffer()
self.write(data)
return self.serial.readline()
[docs] def get_int(self, data):
string = self.write_then_read(data)
first = string.find(b'/0') + 3
last = string.rfind(b'\x03\r\n')
as_int = int(string[first:last])
return as_int
[docs] def read(self):
return self.serial.read_all()
[docs] def do(self, command):
return self.write_then_read(self.programmify((command)))
def __repr__(self):
return '{}(address={}, dev="{}", baudrate={})'.format(
type(self).__name__, self.address, self.name, self._baudrate)
#########################################
### Higher-level functions for humans ###
#########################################
[docs] def pdegrees_per_second(self, dps):
"""Define velocity in degrees per second"""
velocity = self._degrees_per_sec_to_velocity(dps)
self.pvelocity(velocity)
[docs] def degrees_per_second(self, dps):
"""Define velocity in degrees per second"""
velocity = self._degrees_per_sec_to_velocity(dps)
self.velocity(velocity)
[docs] def prpm(self, desired_rpm):
"""Sets the velocity so that the desired rpm will be achieved."""
velocity = self._rpm_to_velocity(desired_rpm)
self.pvelocity(velocity)
[docs] def rpm(self, desired_rpm):
"""Sets the velocity so that the desired rpm will be achieved."""
velocity = self._rpm_to_velocity(desired_rpm)
self.velocity(velocity)
[docs] def pspin(self):
self.program.append(commands.move_relative_positive(0))
self._fake.spin()
def _velocity_to_degrees_per_sec(self, velocity):
one_rps = self.one_revolution_per_second
dps = velocity * 360 / one_rps
return dps
[docs] def init_settings(self, microsteps=None, current=None, degps=None, acc=None):
"""Set the settings we want the radar to operate under"""
# encoder position is meaningful from 0 to 7999 because it's a
# 2000-step encoder.
#
# encoder ratio is described in Appendix B of the R356 manual:
# ratio = step_per_revolution * microsteps / (CPR * 4) * 1000
# for a 1.8 degree encoder, step_per_revolution = 200
# for our encoder, CPR = 2000
#
# we also need to multiply by any gear reduction we're doing.
microsteps = microsteps or self.MICROSTEPS
current = current or self.CURRENT
acc = acc or self.ACCELERATION
self.pnew_program()
self.pcurrent(current)
encoder_ratio = ((self.MOTOR_STEPS_PER_REVOLUTION * microsteps)
/ (self.CPR * 4)
* 1000
* self.REDUCTION
)
self.pmicrosteps_per_step(microsteps)
self.pencoder_ratio(encoder_ratio)
if degps is not None:
self.pdegrees_per_second(degps)
self.pacceleration(acc)
self.run_current_program()
@property
def is_spinning(self):
status = self.get_controller_status()
if not status:
# this is not documented by the motor, but it seems that sometimes
# the motor decides not to respond. I suspect this happens
# whenever the motor is processing a command and then arrives at
# its home position.
return True
return status[3] != 96
class _Mock(object):
def __getattr__(self, name):
return _Mock()
def __setattr__(self, name, val):
pass
def __call__(self, *args, **kwargs):
pass
[docs]class FakeR356(R356):
"""A fake R356 motor so that testing can be done without a motor hooked up.
This emulates a motor as best as I could.
"""
def __init__(self, address=1, dev='/dev/ttyUSB0', baudrate=9600):
"""
address is a number: usually 1.
"""
self.address = address
self.name = dev
self._baudrate = baudrate
self.start = commands.start(self.address)
self.serial = None
self.program = []
self._current = None
self._encoder_ratio = None
self._velocity = 0
self._acceleration = None
# these times are needed to know the position of the motor.
# the motor starts accelerating
self._t_s = None
# acceleration stop time (when motor is at constant velocity)
self._t_a_stop = None
# deceleration start time (motor starts slowing down)
self._t_d_start = None
self._t_f = None
self._initial_position = 0
self._fake = _Mock()
self._direction = 1
self.init_settings()
self.zero()
[docs] def go_home(self, num_steps=1000000):
self.zero()
[docs] def zero(self):
"""Sets current position to 0"""
self._initial_position = 0
self._t_s = time.time()
self._t_a_stop = self._t_s
self._t_d_start = self._t_s
self._t_f = self._t_s
[docs] def get_degrees_per_second(self):
return ticks_to_deg(self._velocity)
[docs] def set_initial_position(self, pos):
self._initial_position = pos * self._encoder_ratio / 1000
def _calc_movement(self, steps):
"""Calculates the times that the motor's acceleration changes. The
motor will speed up until it reaches the max velocity, then run at the
max velocity, then slow down until the velocity is 0."""
# the times calculated here are just solving kinematic equations. Easy
# peasy lemon squeazy.
# start spinning
self._t_s = time.time()
# divide by 1000 because of the way encoder ratio is figured (check out
# appendix B; mostly it was a guess that worked out honestly)
steps *= self._encoder_ratio / 1000
# get the units to microsteps/s
a = self._acceleration * self.ACCELERATION_RATIO
v = self._velocity
# final position, in units of microsteps
x_half = steps / 2
# position when the max velocity is reached under constant acceleration
x_vmax = 1/2 * v**2 / a
if x_vmax >= x_half:
# we're just going to accelerate until we're at x_half then
# decelerate for the same amount of time (to stop again)
# time for acceleration to stop
t_a_stop = math.sqrt(2 * x_half / a)
self._t_a_stop = self._t_s + t_a_stop
self._t_d_start = self._t_a_stop
self._t_f = self._t_d_start + t_a_stop
else:
# time required for acceleration to stop
t_a_stop = v / a
# distance traveled with constant velocity
x_constv = steps - x_vmax*2
if v == 0:
t_constv = 0
else:
t_constv = x_constv / v
self._t_a_stop = self._t_s + t_a_stop
self._t_d_start = self._t_a_stop + t_constv
self._t_f = self._t_d_start + t_a_stop
assert self._t_f >= self._t_d_start >= self._t_a_stop >= self._t_s
def _time_diffs(self):
"""Return the distance from self._t_s all the times are."""
t0 = 0
t1 = self._t_a_stop - self._t_s
t2 = self._t_d_start - self._t_s
t3 = self._t_f - self._t_s
return t0, t1, t2, t3
[docs] def positive(self, steps):
if self._velocity is not None:
self.set_initial_position(self.get_position())
self._direction = 1
if steps == 0:
# set stop time to 10000 revolutions
steps = 8000 * 10000
self._calc_movement(steps)
[docs] def negative(self, steps):
self.set_initial_position(self.get_position())
self._direction = -1
self._calc_movement(steps)
[docs] def stop(self):
if self._velocity is not None:
self._t_f = time.time()
self.get_position()
self.zero()
self._reset()
def _reset(self):
self.zero()
[docs] def get_position(self, now=None):
"""
This function, along with self._calc_movement(), are the most important
functions for the FakeR356.
"""
if now is None:
now = time.time()
t0, t1, t2, t3 = self._time_diffs()
t = now - self._t_s
a = self._direction * self._acceleration * self.ACCELERATION_RATIO
v = self._direction * self._velocity
# initial
x0 = self._initial_position
# position when acceleration is over
x1 = x0 + 1 / 2 * a * (t1)**2
# position when constant velocity part is over
x2 = x1 + v * (self._t_d_start - self._t_a_stop)
# final position
# I'm cheating because I know that the distances of acceleration and
# deceleration are the same.
xf = x2 + (x1 - x0)
if now >= self._t_f:
# we're at the final position
x = xf
elif now >= self._t_d_start:
# decelerating now
# gotta get the correct velocity.
assert t >= t2
if self._t_a_stop == self._t_d_start:
v = t1 * a
del_t = (t - t2)
del_t_2 = del_t**2
x = x2 - 1/2*a*(del_t_2) + v*del_t
elif now >= self._t_a_stop:
assert t >= t1
# constant velocity
x = x1 + v * (now - self._t_a_stop)
else:
# accelerating
assert t >= t0
x = x0 + 1/2 * a * (now - self._t_s) ** 2
encoder_ticks = x / self._encoder_ratio * 1000
return int(round(encoder_ticks))
[docs] def get_actual_position(self):
return self.get_position()
def __repr__(self):
return '{}(address={}, dev="{}", baudrate={})'.format(
type(self).__name__, self.address, self.name, self._baudrate)
[docs] def do(self, cmd):
"""Not going to actually do anything"""
pass
[docs] def run_current_program(self):
"""Don't run the current program"""
pass
@property
def is_spinning(self):
if self._t_f is None:
return False
now = time.time()
return now < self._t_f
[docs]def ticks_to_deg(ticks):
"""Translates ticks to degrees"""
return ((ticks * 360.0 / R356.TICKS_PER_REVOLUTION)) % 360
TPR = R356.TICKS_PER_REVOLUTION
assert ticks_to_deg(TPR) == 0
assert ticks_to_deg(TPR / 2) == 180
assert ticks_to_deg(TPR / 4) == 90
assert ticks_to_deg(TPR + TPR / 4) == 90
assert ticks_to_deg(2 * TPR) == 0
[docs]def deg_to_ticks(degrees):
"""Translates degrees to ticks"""
return int(round(degrees / 360.0 * R356.TICKS_PER_REVOLUTION))
assert deg_to_ticks(360) == TPR
assert deg_to_ticks(720) == 2 * TPR
del TPR
[docs]class Motor(object):
"""
An abstraction over the R356, so the user can think in degrees instead
of stepper positions
"""
def __init__(self, motor, home_angle=0):
"""
Args:
motor: BaseR356 object
"""
self.motor = motor
self._degrees = 0
self.home_angle = home_angle
self.position_valid = False
# has anyone been warned about a bad position?
self._warn_period = 100000
self._noticed_times = 0
[docs] def point(self, degrees):
"""From a stopped state, point a different direction."""
if not self.position_valid:
logger.warning(
"The motor's position is not in a known state; the returned "
"position is inaccurate."
)
desired = degrees % 360
current = self.get_actual_position()
deg_needed = (desired - current) % 360
if deg_needed == 0:
pass
elif deg_needed < 180:
self.positive(deg_needed)
else:
self.negative(360 - deg_needed)
[docs] def positive(self, degrees):
ticks = deg_to_ticks(degrees)
self.motor.positive(ticks)
[docs] def negative(self, degrees):
ticks = deg_to_ticks(degrees)
self.motor.negative(ticks)
def __get_pos(self, actual=False):
if actual:
pos = self.motor.get_actual_position()
else:
pos = self.motor.get_position()
if not self.position_valid:
if self._noticed_times % self._warn_period == 0:
logger.warning(
"The motor's position is not in a known state; the "
" returned position is inaccurate."
)
self._noticed_times += 1
# degrees, counterclockwise from north.
degrees_ccw = (ticks_to_deg(pos) + self.home_angle)
# azimuth angle is defined clockwise, not counter-clockwise
degrees_cw = (-degrees_ccw) % 360
return degrees_cw
[docs] def get_position(self):
"""Return the current position, in degrees."""
return self.__get_pos(actual=False)
[docs] def get_actual_position(self):
return self.__get_pos(actual=True)
[docs] def home(self):
self.motor.home()
self.position_valid = True
[docs] def spin(self):
self.motor.spin()
[docs] def stop(self):
self.position_valid = False
self.motor.stop()
[docs] def get_acceleration(self):
return self.motor.get_acceleration()
[docs] def acceleration(self, acc):
self.motor.acceleration(acc)
[docs] def get_degrees_per_second(self):
return self.motor.get_degrees_per_second()
[docs] def degrees_per_second(self, deg):
return self.motor.degrees_per_second(deg)
# def reverse(self, direction):
# self.motor.reverse(direction)
@property
def is_spinning(self):
return self.motor.is_spinning
if __name__ == '__main__':
logging.basicConfig(
level=logging.DEBUG,
format=logging.BASIC_FORMAT,
stream=sys.stdout
)
c = controller = R356()
motor = Motor(c)
fake = FakeR356()
print('the motor is available as "motor"')
print('the controller is available as "c"')
print('the fake is available as "fake"')
import IPython
IPython.embed()