first implementation

This commit is contained in:
2022-05-11 23:10:25 +02:00
parent 4930a3fd88
commit c32cf40e12
16 changed files with 2516 additions and 0 deletions

1
donkeycar/__init__.py Normal file
View File

@ -0,0 +1 @@
from . import utils

View File

@ -0,0 +1 @@
from . import pins

219
donkeycar/parts/actuator.py Normal file
View 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

File diff suppressed because it is too large Load Diff

237
donkeycar/utils.py Normal file
View 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