first implementation
This commit is contained in:
1
donkeycar/__init__.py
Normal file
1
donkeycar/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import utils
|
1
donkeycar/parts/__init__.py
Normal file
1
donkeycar/parts/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import pins
|
219
donkeycar/parts/actuator.py
Normal file
219
donkeycar/parts/actuator.py
Normal file
@ -0,0 +1,219 @@
|
||||
"""
|
||||
actuators.py
|
||||
Classes to control the motors and servos. These classes
|
||||
are wrapped in a mixer class before being used in the drive loop.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
import donkeycar as dk
|
||||
from donkeycar.parts.pins import PwmPin, PinState
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
#
|
||||
# pwm/duty-cycle/pulse
|
||||
# - Standard RC servo pulses range from 1 millisecond (full reverse)
|
||||
# to 2 milliseconds (full forward) with 1.5 milliseconds being neutral (stopped).
|
||||
# - These pulses are typically send at 50 hertz (every 20 milliseconds).
|
||||
# - This means that, using the standard 50hz frequency, a 1 ms pulse
|
||||
# represents a 5% duty cycle and a 2 ms pulse represents a 10% duty cycle.
|
||||
# - The important part is the length of the pulse;
|
||||
# it must be in the range of 1 ms to 2ms.
|
||||
# - So this means that if a different frequency is used, then the duty cycle
|
||||
# must be adjusted in order to get the 1ms to 2ms pulse.
|
||||
# - For instance, if a 60hz frequency is used, then a 1 ms pulse requires
|
||||
# a duty cycle of 0.05 * 60 / 50 = 0.06 (6%) duty cycle
|
||||
# - We default the frequency of our PCA9685 to 60 hz, so pulses in
|
||||
# config are generally based on 60hz frequency and 12 bit values.
|
||||
# We use 12 bit values because the PCA9685 has 12 bit resolution.
|
||||
# So a 1 ms pulse is 0.06 * 4096 ~= 246, a neutral pulse of 0.09 duty cycle
|
||||
# is 0.09 * 4096 ~= 367 and full forward pulse of 0.12 duty cycles
|
||||
# is 0.12 * 4096 ~= 492
|
||||
# - These are generalizations that are useful for understanding the underlying
|
||||
# api call arguments. The final choice of duty-cycle/pulse length depends
|
||||
# on your hardware and perhaps your strategy (you may not want to go too fast,
|
||||
# and so you may choose is low max throttle pwm)
|
||||
#
|
||||
|
||||
def duty_cycle(pulse_ms: float, frequency_hz: float) -> float:
|
||||
"""
|
||||
Calculate the duty cycle, 0 to 1, of a pulse given
|
||||
the frequency and the pulse length
|
||||
|
||||
:param pulse_ms:float the desired pulse length in milliseconds
|
||||
:param frequency_hz:float the pwm frequency in hertz
|
||||
:return:float duty cycle in range 0 to 1
|
||||
"""
|
||||
ms_per_cycle = 1000 / frequency_hz
|
||||
duty = pulse_ms / ms_per_cycle
|
||||
return duty
|
||||
|
||||
|
||||
def pulse_ms(pulse_bits: int) -> float:
|
||||
"""
|
||||
Calculate pulse width in milliseconds given a
|
||||
12bit pulse (as a PCA9685 would use).
|
||||
Donkeycar throttle and steering PWM values are
|
||||
based on PCA9685 12bit pulse values, where
|
||||
0 is zero duty cycle and 4095 is 100% duty cycle.
|
||||
|
||||
:param pulse_bits:int 12bit integer in range 0 to 4095
|
||||
:return:float pulse length in milliseconds
|
||||
"""
|
||||
if pulse_bits < 0 or pulse_bits > 4095:
|
||||
raise ValueError("pulse_bits must be in range 0 to 4095 (12bit integer)")
|
||||
return pulse_bits / 4095
|
||||
|
||||
|
||||
class PulseController:
|
||||
"""
|
||||
Controller that provides a servo PWM pulse using the given PwmPin
|
||||
See pins.py for pin provider implementations.
|
||||
"""
|
||||
|
||||
def __init__(self, pwm_pin: PwmPin, pwm_scale: float = 1.0, pwm_inverted: bool = False) -> None:
|
||||
"""
|
||||
:param pwm_pin:PwnPin pin that will emit the pulse.
|
||||
:param pwm_scale:float scaling the 12 bit pulse value to compensate
|
||||
for non-standard pwm frequencies.
|
||||
:param pwm_inverted:bool True to invert the duty cycle
|
||||
"""
|
||||
self.pwm_pin = pwm_pin
|
||||
self.scale = pwm_scale
|
||||
self.inverted = pwm_inverted
|
||||
self.started = pwm_pin.state() != PinState.NOT_STARTED
|
||||
|
||||
def set_pulse(self, pulse: int) -> None:
|
||||
"""
|
||||
Set the length of the pulse using a 12 bit integer (0..4095)
|
||||
:param pulse:int 12bit integer (0..4095)
|
||||
"""
|
||||
if pulse < 0 or pulse > 4095:
|
||||
raise ValueError("pulse must be in range 0 to 4095")
|
||||
|
||||
if not self.started:
|
||||
self.pwm_pin.start()
|
||||
self.started = True
|
||||
if self.inverted:
|
||||
pulse = 4095 - pulse
|
||||
self.pwm_pin.duty_cycle(int(pulse * self.scale) / 4095)
|
||||
|
||||
def run(self, pulse: int) -> None:
|
||||
"""
|
||||
Set the length of the pulse using a 12 bit integer (0..4095)
|
||||
:param pulse:int 12bit integer (0..4095)
|
||||
"""
|
||||
self.set_pulse(pulse)
|
||||
|
||||
|
||||
class PWMSteering:
|
||||
"""
|
||||
Wrapper over a PWM pulse controller to convert angles to PWM pulses.
|
||||
"""
|
||||
LEFT_ANGLE = -1
|
||||
RIGHT_ANGLE = 1
|
||||
|
||||
def __init__(self, controller, left_pulse, right_pulse):
|
||||
|
||||
if controller is None:
|
||||
raise ValueError("PWMSteering requires a set_pulse controller to be passed")
|
||||
set_pulse = getattr(controller, "set_pulse", None)
|
||||
if set_pulse is None or not callable(set_pulse):
|
||||
raise ValueError("controller must have a set_pulse method")
|
||||
|
||||
self.controller = controller
|
||||
self.left_pulse = left_pulse
|
||||
self.right_pulse = right_pulse
|
||||
self.pulse = dk.utils.map_range(0, self.LEFT_ANGLE, self.RIGHT_ANGLE,
|
||||
self.left_pulse, self.right_pulse)
|
||||
self.running = True
|
||||
logger.info('PWM Steering created')
|
||||
|
||||
def update(self):
|
||||
while self.running:
|
||||
self.controller.set_pulse(self.pulse)
|
||||
|
||||
def run_threaded(self, angle):
|
||||
# map absolute angle to angle that vehicle can implement.
|
||||
self.pulse = dk.utils.map_range(angle,
|
||||
self.LEFT_ANGLE, self.RIGHT_ANGLE,
|
||||
self.left_pulse, self.right_pulse)
|
||||
|
||||
def run(self, angle):
|
||||
self.run_threaded(angle)
|
||||
self.controller.set_pulse(self.pulse)
|
||||
|
||||
def shutdown(self):
|
||||
# set steering straight
|
||||
self.pulse = 0
|
||||
time.sleep(0.3)
|
||||
self.running = False
|
||||
|
||||
|
||||
class PWMThrottle:
|
||||
"""
|
||||
Wrapper over a PWM pulse controller to convert -1 to 1 throttle
|
||||
values to PWM pulses.
|
||||
"""
|
||||
MIN_THROTTLE = -1
|
||||
MAX_THROTTLE = 1
|
||||
|
||||
def __init__(self, controller, max_pulse, min_pulse, zero_pulse):
|
||||
|
||||
if controller is None:
|
||||
raise ValueError("PWMThrottle requires a set_pulse controller to be passed")
|
||||
set_pulse = getattr(controller, "set_pulse", None)
|
||||
if set_pulse is None or not callable(set_pulse):
|
||||
raise ValueError("controller must have a set_pulse method")
|
||||
|
||||
self.controller = controller
|
||||
self.max_pulse = max_pulse
|
||||
self.min_pulse = min_pulse
|
||||
self.zero_pulse = zero_pulse
|
||||
self.pulse = zero_pulse
|
||||
|
||||
# send zero pulse to calibrate ESC
|
||||
logger.info("Init ESC")
|
||||
self.controller.set_pulse(self.max_pulse)
|
||||
time.sleep(0.01)
|
||||
self.controller.set_pulse(self.min_pulse)
|
||||
time.sleep(0.01)
|
||||
self.controller.set_pulse(self.zero_pulse)
|
||||
time.sleep(1)
|
||||
self.running = True
|
||||
logger.info('PWM Throttle created')
|
||||
|
||||
def update(self):
|
||||
while self.running:
|
||||
self.controller.set_pulse(self.pulse)
|
||||
|
||||
def run_threaded(self, throttle):
|
||||
if throttle > 0:
|
||||
self.pulse = dk.utils.map_range(throttle, 0, self.MAX_THROTTLE,
|
||||
self.zero_pulse, self.max_pulse)
|
||||
else:
|
||||
self.pulse = dk.utils.map_range(throttle, self.MIN_THROTTLE, 0,
|
||||
self.min_pulse, self.zero_pulse)
|
||||
|
||||
def run(self, throttle):
|
||||
self.run_threaded(throttle)
|
||||
self.controller.set_pulse(self.pulse)
|
||||
|
||||
def shutdown(self):
|
||||
# stop vehicle
|
||||
self.run(0)
|
||||
self.running = False
|
||||
|
||||
|
||||
class MockController(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def run(self, pulse):
|
||||
pass
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
1043
donkeycar/parts/pins.py
Normal file
1043
donkeycar/parts/pins.py
Normal file
File diff suppressed because it is too large
Load Diff
237
donkeycar/utils.py
Normal file
237
donkeycar/utils.py
Normal file
@ -0,0 +1,237 @@
|
||||
'''
|
||||
utils.py
|
||||
|
||||
Functions that don't fit anywhere else.
|
||||
|
||||
'''
|
||||
from io import BytesIO
|
||||
import os
|
||||
import glob
|
||||
import socket
|
||||
import zipfile
|
||||
import sys
|
||||
import itertools
|
||||
import subprocess
|
||||
import math
|
||||
import random
|
||||
import time
|
||||
import signal
|
||||
import logging
|
||||
from typing import List, Any, Tuple, Union
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ONE_BYTE_SCALE = 1.0 / 255.0
|
||||
|
||||
|
||||
class EqMemorizedString:
|
||||
""" String that remembers what it was compared against """
|
||||
|
||||
def __init__(self, string):
|
||||
self.string = string
|
||||
self.mem = set()
|
||||
|
||||
def __eq__(self, other):
|
||||
self.mem.add(other)
|
||||
return self.string == other
|
||||
|
||||
def mem_as_str(self):
|
||||
return ', '.join(self.mem)
|
||||
|
||||
|
||||
|
||||
|
||||
'''
|
||||
FILES
|
||||
'''
|
||||
|
||||
|
||||
def most_recent_file(dir_path, ext=''):
|
||||
'''
|
||||
return the most recent file given a directory path and extension
|
||||
'''
|
||||
query = dir_path + '/*' + ext
|
||||
newest = min(glob.iglob(query), key=os.path.getctime)
|
||||
return newest
|
||||
|
||||
|
||||
def make_dir(path):
|
||||
real_path = os.path.expanduser(path)
|
||||
if not os.path.exists(real_path):
|
||||
os.makedirs(real_path)
|
||||
return real_path
|
||||
|
||||
|
||||
def zip_dir(dir_path, zip_path):
|
||||
"""
|
||||
Create and save a zipfile of a one level directory
|
||||
"""
|
||||
file_paths = glob.glob(dir_path + "/*") # create path to search for files.
|
||||
|
||||
zf = zipfile.ZipFile(zip_path, 'w')
|
||||
dir_name = os.path.basename(dir_path)
|
||||
for p in file_paths:
|
||||
file_name = os.path.basename(p)
|
||||
zf.write(p, arcname=os.path.join(dir_name, file_name))
|
||||
zf.close()
|
||||
return zip_path
|
||||
|
||||
|
||||
'''
|
||||
BINNING
|
||||
functions to help converte between floating point numbers and categories.
|
||||
'''
|
||||
|
||||
|
||||
def map_range(x, X_min, X_max, Y_min, Y_max):
|
||||
'''
|
||||
Linear mapping between two ranges of values
|
||||
'''
|
||||
X_range = X_max - X_min
|
||||
Y_range = Y_max - Y_min
|
||||
XY_ratio = X_range / Y_range
|
||||
|
||||
y = ((x - X_min) / XY_ratio + Y_min) // 1
|
||||
|
||||
return int(y)
|
||||
|
||||
|
||||
def map_range_float(x, X_min, X_max, Y_min, Y_max):
|
||||
'''
|
||||
Same as map_range but supports floats return, rounded to 2 decimal places
|
||||
'''
|
||||
X_range = X_max - X_min
|
||||
Y_range = Y_max - Y_min
|
||||
XY_ratio = X_range / Y_range
|
||||
|
||||
y = ((x - X_min) / XY_ratio + Y_min)
|
||||
|
||||
# print("y= {}".format(y))
|
||||
|
||||
return round(y, 2)
|
||||
|
||||
|
||||
'''
|
||||
ANGLES
|
||||
'''
|
||||
|
||||
|
||||
def norm_deg(theta):
|
||||
while theta > 360:
|
||||
theta -= 360
|
||||
while theta < 0:
|
||||
theta += 360
|
||||
return theta
|
||||
|
||||
|
||||
DEG_TO_RAD = math.pi / 180.0
|
||||
|
||||
|
||||
def deg2rad(theta):
|
||||
return theta * DEG_TO_RAD
|
||||
|
||||
|
||||
'''
|
||||
VECTORS
|
||||
'''
|
||||
|
||||
|
||||
def dist(x1, y1, x2, y2):
|
||||
return math.sqrt(math.pow(x2 - x1, 2) + math.pow(y2 - y1, 2))
|
||||
|
||||
|
||||
'''
|
||||
NETWORKING
|
||||
'''
|
||||
|
||||
|
||||
def my_ip():
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.connect(('192.0.0.8', 1027))
|
||||
return s.getsockname()[0]
|
||||
|
||||
|
||||
|
||||
'''
|
||||
OTHER
|
||||
'''
|
||||
|
||||
|
||||
def map_frange(x, X_min, X_max, Y_min, Y_max):
|
||||
'''
|
||||
Linear mapping between two ranges of values
|
||||
'''
|
||||
X_range = X_max - X_min
|
||||
Y_range = Y_max - Y_min
|
||||
XY_ratio = X_range / Y_range
|
||||
|
||||
y = ((x - X_min) / XY_ratio + Y_min)
|
||||
|
||||
return y
|
||||
|
||||
|
||||
def merge_two_dicts(x, y):
|
||||
"""Given two dicts, merge them into a new dict as a shallow copy."""
|
||||
z = x.copy()
|
||||
z.update(y)
|
||||
return z
|
||||
|
||||
|
||||
def param_gen(params):
|
||||
'''
|
||||
Accepts a dictionary of parameter options and returns
|
||||
a list of dictionary with the permutations of the parameters.
|
||||
'''
|
||||
for p in itertools.product(*params.values()):
|
||||
yield dict(zip(params.keys(), p))
|
||||
|
||||
|
||||
def run_shell_command(cmd, cwd=None, timeout=15):
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
|
||||
out = []
|
||||
err = []
|
||||
|
||||
try:
|
||||
proc.wait(timeout=timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
kill(proc.pid)
|
||||
|
||||
for line in proc.stdout.readlines():
|
||||
out.append(line.decode())
|
||||
|
||||
for line in proc.stderr.readlines():
|
||||
err.append(line)
|
||||
return out, err, proc.pid
|
||||
|
||||
|
||||
def kill(proc_id):
|
||||
os.kill(proc_id, signal.SIGINT)
|
||||
|
||||
|
||||
def eprint(*args, **kwargs):
|
||||
print(*args, file=sys.stderr, **kwargs)
|
||||
|
||||
|
||||
|
||||
"""
|
||||
Timers
|
||||
"""
|
||||
|
||||
|
||||
class FPSTimer(object):
|
||||
def __init__(self):
|
||||
self.t = time.time()
|
||||
self.iter = 0
|
||||
|
||||
def reset(self):
|
||||
self.t = time.time()
|
||||
self.iter = 0
|
||||
|
||||
def on_frame(self):
|
||||
self.iter += 1
|
||||
if self.iter == 100:
|
||||
e = time.time()
|
||||
print('fps', 100.0 / (e - self.t))
|
||||
self.t = time.time()
|
||||
self.iter = 0
|
Reference in New Issue
Block a user