├── donkeycar ├── gym │ ├── __init__.py │ ├── remote_controller.py │ └── gym_real.py ├── parts │ ├── __init__.py │ ├── web_controller │ │ └── templates │ │ │ ├── static │ │ │ ├── img_placeholder.jpg │ │ │ ├── donkeycar-logo-sideways.png │ │ │ ├── bootstrap │ │ │ │ └── 3.3.7 │ │ │ │ │ └── fonts │ │ │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ │ │ └── glyphicons-halflings-regular.woff2 │ │ │ └── style.css │ │ │ ├── pilots_list.html │ │ │ ├── session_list.html │ │ │ ├── vehicle_list.html │ │ │ ├── home.html │ │ │ ├── base.html │ │ │ ├── wsTest.html │ │ │ └── base_fpv.html │ ├── pipe.py │ ├── throttle_filter.py │ ├── sombrero.py │ ├── file_watcher.py │ ├── explode.py │ ├── pytorch │ │ ├── torch_utils.py │ │ ├── torch_train.py │ │ └── ResNet18.py │ ├── graph.py │ ├── logger.py │ ├── ros.py │ ├── fps.py │ ├── launch.py │ ├── perfmon.py │ ├── serial_controller.py │ ├── behavior.py │ ├── simulation.py │ ├── leopard_imaging.py │ ├── tfmini.py │ ├── odometer.py │ ├── voice_control │ │ └── alexa.py │ ├── fast_stretch.py │ ├── pigpio_enc.py │ ├── teensy.py │ ├── coral.py │ ├── image.py │ ├── dgym.py │ ├── imu.py │ ├── led_status.py │ ├── text_writer.py │ └── realsense2.py ├── contrib │ ├── __init__.py │ └── robohat │ │ ├── lib │ │ └── adafruit_logging.mpy │ │ └── rear_light.py ├── utilities │ ├── __init__.py │ ├── dk_platform.py │ └── deprecated.py ├── management │ ├── ui │ │ ├── __init__.py │ │ ├── ui.kv │ │ ├── ui.py │ │ └── rc_file_handler.py │ ├── __init__.py │ └── tub_web │ │ ├── static │ │ ├── bootstrap │ │ │ └── 3.3.7 │ │ │ │ └── fonts │ │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ │ └── glyphicons-halflings-regular.woff2 │ │ └── style.css │ │ ├── tubs.html │ │ ├── base.html │ │ └── tub.html ├── tests │ ├── test_parts.py │ ├── __init__.py │ ├── tub │ │ └── tub.tar.gz │ ├── pytest.ini │ ├── test_actuator.py │ ├── test_web_controller.py │ ├── test_controller.py │ ├── test_template.py │ ├── test_catalog_v2.py │ ├── test_tubwriter.py │ ├── test_odometer.py │ ├── test_vehicle.py │ ├── test_memory.py │ ├── test_launch.py │ ├── setup.py │ ├── test_tub_v2.py │ ├── test_datastore_v2.py │ ├── test_scripts.py │ ├── test_lidar.py │ ├── test_keras.py │ ├── test_seekable_v2.py │ ├── test_telemetry.py │ └── test_circular_buffer.py ├── pipeline │ ├── __init__.py │ └── augmentations.py ├── templates │ ├── myconfig.py │ ├── train.py │ ├── calibration_odometry.json │ ├── cfg_arduino_drive.py │ ├── arduino_drive.py │ ├── cfg_square.py │ └── just_drive.py ├── __init__.py ├── geom.py ├── benchmarks │ ├── tub_v2.py │ └── tub.py ├── memory.py └── config.py ├── .coveragerc ├── Makefile ├── scripts ├── disable_js_mouse.sh ├── tflite_convert.py ├── profile_coral.py ├── profile.py ├── tflite_profile.py ├── salient_vis_listener.py ├── graph_listener.py ├── remote_cam_view_tcp.py ├── convert_to_tflite.py ├── preview_augumentations.py ├── remote_cam_view.py ├── freeze_model.py ├── multi_train.py ├── pigpio_donkey.py └── convert_to_tub_v2.py ├── MANIFEST.in ├── pyproject.toml ├── .github ├── linters │ └── .python-black └── workflows │ ├── superlinter.yml │ └── python-package-conda.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── setup.cfg └── CONTRIBUTING.md /donkeycar/gym/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /donkeycar/parts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /donkeycar/contrib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /donkeycar/utilities/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /donkeycar/management/ui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /donkeycar/tests/test_parts.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /donkeycar/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | omit = 4 | donkeycar/tests/* -------------------------------------------------------------------------------- /donkeycar/pipeline/__init__.py: -------------------------------------------------------------------------------- 1 | # The new donkey car training pipeline. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | tests: 4 | pytest 5 | 6 | package: 7 | python setup.py sdist 8 | 9 | -------------------------------------------------------------------------------- /donkeycar/management/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | -------------------------------------------------------------------------------- /scripts/disable_js_mouse.sh: -------------------------------------------------------------------------------- 1 | xinput set-prop "Sony PLAYSTATION(R)3 Controller" "Device Enabled" 0 2 | 3 | -------------------------------------------------------------------------------- /donkeycar/tests/tub/tub.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autorope/donkeycar/HEAD/donkeycar/tests/tub/tub.tar.gz -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include donkeycar/templates/* 2 | include scripts 3 | recursive-include donkeycar/parts/web_controller/templates/ * 4 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /donkeycar/contrib/robohat/lib/adafruit_logging.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autorope/donkeycar/HEAD/donkeycar/contrib/robohat/lib/adafruit_logging.mpy -------------------------------------------------------------------------------- /donkeycar/parts/web_controller/templates/static/img_placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autorope/donkeycar/HEAD/donkeycar/parts/web_controller/templates/static/img_placeholder.jpg -------------------------------------------------------------------------------- /donkeycar/tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | filterwarnings = 3 | ignore::DeprecationWarning 4 | ignore::FutureWarning 5 | 6 | log_cli = True 7 | log_cli_level = INFO 8 | reruns = 3 9 | -------------------------------------------------------------------------------- /donkeycar/parts/web_controller/templates/static/donkeycar-logo-sideways.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autorope/donkeycar/HEAD/donkeycar/parts/web_controller/templates/static/donkeycar-logo-sideways.png -------------------------------------------------------------------------------- /donkeycar/management/tub_web/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autorope/donkeycar/HEAD/donkeycar/management/tub_web/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /donkeycar/management/tub_web/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autorope/donkeycar/HEAD/donkeycar/management/tub_web/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /donkeycar/management/tub_web/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autorope/donkeycar/HEAD/donkeycar/management/tub_web/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /donkeycar/parts/web_controller/templates/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autorope/donkeycar/HEAD/donkeycar/parts/web_controller/templates/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /donkeycar/parts/web_controller/templates/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autorope/donkeycar/HEAD/donkeycar/parts/web_controller/templates/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /donkeycar/parts/web_controller/templates/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autorope/donkeycar/HEAD/donkeycar/parts/web_controller/templates/static/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /.github/linters/.python-black: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 80 3 | target-version = ['py37'] 4 | multi_line_output = 3 5 | include_trailing_comma = true 6 | force_grid_wrap = 0 7 | use_parentheses = true 8 | ensure_newline_before_comments = true 9 | skip-string-normalization = true 10 | 11 | -------------------------------------------------------------------------------- /donkeycar/templates/myconfig.py: -------------------------------------------------------------------------------- 1 | # """ 2 | # My CAR CONFIG 3 | 4 | # This file is read by your car application's manage.py script to change the car 5 | # performance 6 | 7 | # If desired, all config overrides can be specified here. 8 | # The update operation will not touch this file. 9 | # """ 10 | 11 | -------------------------------------------------------------------------------- /donkeycar/parts/pipe.py: -------------------------------------------------------------------------------- 1 | class Pipe: 2 | """ 3 | Just pipe all inputs to the output, so they can be renamed. 4 | """ 5 | def run(self, *args): 6 | # seems to be a python bug that takes a single argument 7 | # return makes it into two element tuple with empty last element. 8 | return args if len(args) > 1 else args[0] 9 | -------------------------------------------------------------------------------- /donkeycar/management/tub_web/tubs.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | {% block content %} 4 | 5 | 6 |
7 | 12 | 13 |
14 | 15 | 16 | 17 | {% end %} 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | env/* 3 | data/* 4 | dist/* 5 | 6 | *.pyc 7 | .ipynb_checkpoints 8 | *.egg-info/ 9 | .git.bfg-report 10 | .DS_Store 11 | lag_log.csv 12 | 13 | .cache 14 | site/* 15 | build 16 | 17 | #IDE 18 | .idea 19 | .spyproject 20 | .pytest_cache 21 | .coverage 22 | .vscode 23 | 24 | # codecov 25 | htmlcov/ 26 | 27 | # PyTorch 28 | lightning_logs 29 | tb_logs 30 | -------------------------------------------------------------------------------- /donkeycar/tests/test_actuator.py: -------------------------------------------------------------------------------- 1 | from .setup import on_pi 2 | 3 | from donkeycar.parts.actuator import PCA9685, PWMSteering, PWMThrottle 4 | import pytest 5 | 6 | 7 | @pytest.mark.skipif(on_pi() == False, reason='Not on RPi') 8 | def test_PCA9685(): 9 | c = PCA9685(0) 10 | 11 | @pytest.mark.skipif(on_pi() == False, reason='Not on RPi') 12 | def test_PWMSteering(): 13 | c = PCA9685(0) 14 | s = PWMSteering(c, 300, 440) 15 | -------------------------------------------------------------------------------- /scripts/tflite_convert.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Usage: 3 | tflite_convert.py --model="mymodel.h5" --out="mymodel.tflite" 4 | 5 | Note: 6 | may require tensorflow > 1.11 or 7 | pip install tf-nightly 8 | ''' 9 | import os 10 | 11 | from docopt import docopt 12 | from donkeycar.parts.interpreter import keras_model_to_tflite 13 | 14 | args = docopt(__doc__) 15 | 16 | in_model = os.path.expanduser(args['--model']) 17 | out_model = os.path.expanduser(args['--out']) 18 | keras_model_to_tflite(in_model, out_model) 19 | 20 | -------------------------------------------------------------------------------- /donkeycar/parts/web_controller/templates/pilots_list.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | {% block content %} 4 |
5 |

Pilots

6 | 7 | 21 | 22 |
23 | {% end %} 24 | -------------------------------------------------------------------------------- /donkeycar/parts/web_controller/templates/session_list.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | {% block content %} 4 |
5 |

Sessions

6 | 7 | 21 | 22 |
23 | {% end %} 24 | -------------------------------------------------------------------------------- /donkeycar/parts/web_controller/templates/vehicle_list.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | {% block content %} 4 |
5 |

Vehicles

6 | 7 | 21 | 22 |
23 | {% end %} 24 | -------------------------------------------------------------------------------- /donkeycar/parts/web_controller/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | {% block content %} 4 |
5 | 6 |

Donkey

7 | 8 |

Your donkey is ready to ride

9 | 10 |

Using this web interface you can do the following

11 | 12 | 16 | 17 | 18 |

Features we are working on include:

19 | 25 |
26 | {% end %} 27 | -------------------------------------------------------------------------------- /donkeycar/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from pyfiglet import Figlet 4 | import logging 5 | 6 | __version__ = '5.2.dev5' 7 | 8 | logging.basicConfig(level=os.environ.get('LOGLEVEL', 'INFO').upper()) 9 | 10 | f = Figlet(font='speed') 11 | 12 | 13 | print(f.renderText('Donkey Car')) 14 | print(f'using donkey v{__version__} ...') 15 | 16 | if sys.version_info.major < 3 or sys.version_info.minor < 11: 17 | msg = f'Donkey Requires Python 3.11 or greater. You are using {sys.version}' 18 | raise ValueError(msg) 19 | 20 | # The default recursion limits in CPython are too small. 21 | sys.setrecursionlimit(10**5) 22 | 23 | from .vehicle import Vehicle 24 | from .memory import Memory 25 | from . import utils 26 | from . import config 27 | from . import contrib 28 | from .config import load_config 29 | -------------------------------------------------------------------------------- /donkeycar/parts/throttle_filter.py: -------------------------------------------------------------------------------- 1 | 2 | class ThrottleFilter(object): 3 | ''' 4 | allow reverse to trigger automatic reverse throttle 5 | ''' 6 | 7 | def __init__(self): 8 | self.reverse_triggered = False 9 | self.last_throttle = 0.0 10 | 11 | def run(self, throttle_in): 12 | if throttle_in is None: 13 | return throttle_in 14 | 15 | throttle_out = throttle_in 16 | 17 | if throttle_out < 0.0: 18 | if not self.reverse_triggered and self.last_throttle < 0.0: 19 | throttle_out = 0.0 20 | self.reverse_triggered = True 21 | else: 22 | self.reverse_triggered = False 23 | 24 | self.last_throttle = throttle_out 25 | return throttle_out 26 | 27 | def shutdown(self): 28 | pass 29 | -------------------------------------------------------------------------------- /donkeycar/utilities/dk_platform.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | 4 | # 5 | # functions to query hardware and os 6 | # 7 | def is_mac(): 8 | return "Darwin" == platform.system() 9 | 10 | # 11 | # read tegra chip id if it exists. 12 | # 13 | def read_chip_id() -> str: 14 | """ 15 | Read the tegra chip id. 16 | On non-tegra platforms this will be blank. 17 | """ 18 | try: 19 | with open("/sys/module/tegra_fuse/parameters/tegra_chip_id", "r") as f: 20 | return next(f) 21 | except FileNotFoundError: 22 | pass 23 | return "" 24 | 25 | _chip_id = None 26 | 27 | def is_jetson() -> bool: 28 | """ 29 | Determine if platform is a jetson 30 | """ 31 | global _chip_id 32 | if _chip_id is None: 33 | _chip_id = read_chip_id() 34 | return _chip_id != "" 35 | -------------------------------------------------------------------------------- /donkeycar/parts/sombrero.py: -------------------------------------------------------------------------------- 1 | class Sombrero: 2 | ''' 3 | A pi hat developed by Adam Conway to manage power, pwm for a Donkeycar 4 | This requires that GPIO 26 is brought low to enable the pwm out. 5 | Because all GPIO modes have to be the same accross code, we use BOARD 6 | mode, which is physical pin 37. 7 | ''' 8 | 9 | def __init__(self): 10 | try: 11 | import RPi.GPIO as GPIO 12 | 13 | GPIO.setmode(GPIO.BOARD) 14 | GPIO.setup(37, GPIO.OUT) 15 | GPIO.output(37, GPIO.LOW) 16 | print("sombrero enabled") 17 | except: 18 | pass 19 | 20 | def __del__(self): 21 | try: 22 | import RPi.GPIO as GPIO 23 | 24 | GPIO.cleanup() 25 | print("sombrero disabled") 26 | except: 27 | pass 28 | -------------------------------------------------------------------------------- /scripts/profile_coral.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from donkeycar.parts.coral import InferenceEngine 3 | from PIL import Image 4 | from donkeycar.utils import FPSTimer 5 | import numpy as np 6 | 7 | def main(): 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument( 10 | '--model', help='File path of Tflite model.', required=True) 11 | parser.add_argument( 12 | '--image', help='File path of the image to be recognized.', required=True) 13 | args = parser.parse_args() 14 | # Initialize engine. 15 | engine = InferenceEngine(args.model) 16 | # Run inference. 17 | img = Image.open(args.image) 18 | result = engine.Inference(np.array(img)) 19 | print("inference result", result) 20 | 21 | timer = FPSTimer() 22 | while True: 23 | engine.Inference(np.array(img)) 24 | timer.on_frame() 25 | 26 | if __name__ == '__main__': 27 | main() -------------------------------------------------------------------------------- /donkeycar/parts/file_watcher.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | class FileWatcher(object): 4 | ''' 5 | Watch a specific file and give a signal when it's modified 6 | ''' 7 | 8 | def __init__(self, filename, verbose=False): 9 | self.modified_time = os.path.getmtime(filename) 10 | self.filename = filename 11 | self.verbose = verbose 12 | 13 | def run(self): 14 | ''' 15 | return True when file changed. Keep in mind that this does not mean that the 16 | file is finished with modification. 17 | ''' 18 | m_time = os.path.getmtime(self.filename) 19 | 20 | if m_time != self.modified_time: 21 | self.modified_time = m_time 22 | if self.verbose: 23 | print(self.filename, "changed.") 24 | return True 25 | 26 | return False 27 | 28 | 29 | -------------------------------------------------------------------------------- /donkeycar/parts/explode.py: -------------------------------------------------------------------------------- 1 | # 2 | # part that explodes a dictionary argument into individually named arguments 3 | # 4 | 5 | 6 | class ExplodeDict: 7 | """ 8 | part that expands a dictionary input argument 9 | into individually named output arguments 10 | """ 11 | def __init__(self, memory, output_prefix = ""): 12 | """ 13 | Break a map into key/value pairs and write 14 | them to the output memory, optionally 15 | prefixing the key on output. 16 | Basically, take a dictionary and write 17 | it to the output. 18 | """ 19 | self.memory = memory 20 | self.prefix = output_prefix 21 | 22 | def run(self, key_values): 23 | if type(key_values) is dict: 24 | for key, value in key_values.items(): 25 | self.memory[self.prefix + key] = value 26 | return None 27 | -------------------------------------------------------------------------------- /donkeycar/templates/train.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Scripts to train a keras model using tensorflow. 4 | Basic usage should feel familiar: train.py --tubs data/ --model models/mypilot.h5 5 | 6 | Usage: 7 | train.py [--tubs=tubs] (--model=) 8 | [--type=(linear|inferred|tensorrt_linear|tflite_linear)] 9 | [--comment=] 10 | 11 | Options: 12 | -h --help Show this screen. 13 | """ 14 | 15 | from docopt import docopt 16 | import donkeycar as dk 17 | from donkeycar.pipeline.training import train 18 | 19 | 20 | def main(): 21 | args = docopt(__doc__) 22 | cfg = dk.load_config() 23 | tubs = args['--tubs'] 24 | model = args['--model'] 25 | model_type = args['--type'] 26 | comment = args['--comment'] 27 | train(cfg, tubs, model, model_type, comment) 28 | 29 | 30 | if __name__ == "__main__": 31 | main() 32 | -------------------------------------------------------------------------------- /donkeycar/tests/test_web_controller.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | import json 4 | import os 5 | from donkeycar.parts.web_controller.web import LocalWebController 6 | import donkeycar.templates.cfg_complete as cfg 7 | from importlib import reload 8 | 9 | @pytest.fixture 10 | def server(): 11 | server = LocalWebController(cfg.WEB_CONTROL_PORT) 12 | return server 13 | 14 | 15 | def test_json_output(server): 16 | result = server.run() 17 | json_result = json.dumps(result) 18 | d = json.loads(json_result) 19 | 20 | assert server.port == 8887 21 | 22 | assert d is not None 23 | assert int(d[0]) == 0 24 | 25 | 26 | def test_web_control_user_defined_port(): 27 | os.environ['WEB_CONTROL_PORT'] = "12345" 28 | reload(cfg) 29 | server = LocalWebController(port=cfg.WEB_CONTROL_PORT) 30 | 31 | assert server.port == 12345 32 | 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6 2 | 3 | WORKDIR /app 4 | 5 | # install donkey with tensorflow (cpu only version) 6 | ADD ./setup.py /app/setup.py 7 | ADD ./README.md /app/README.md 8 | RUN pip install -e .[tf] 9 | 10 | # get testing requirements 11 | RUN pip install -e .[dev] 12 | 13 | # setup jupyter notebook to run without password 14 | RUN pip install jupyter notebook 15 | RUN jupyter notebook --generate-config 16 | RUN echo "c.NotebookApp.password = ''">>/root/.jupyter/jupyter_notebook_config.py 17 | RUN echo "c.NotebookApp.token = ''">>/root/.jupyter/jupyter_notebook_config.py 18 | 19 | # add the whole app dir after install so the pip install isn't updated when code changes. 20 | ADD . /app 21 | 22 | #start the jupyter notebook 23 | CMD jupyter notebook --no-browser --ip 0.0.0.0 --port 8888 --allow-root --notebook-dir=/app/notebooks 24 | 25 | #port for donkeycar 26 | EXPOSE 8887 27 | 28 | #port for jupyter notebook 29 | EXPOSE 8888 -------------------------------------------------------------------------------- /.github/workflows/superlinter.yml: -------------------------------------------------------------------------------- 1 | name: Super-Linter 2 | 3 | # Run this workflow every time a new commit pushed to your repository 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | # Set the job key. The key is displayed as the job name 8 | # when a job name is not provided 9 | super-lint: 10 | # Name the Job 11 | name: Lint code base 12 | # Set the type of machine to run on 13 | runs-on: ubuntu-latest 14 | # Don't flag failure 15 | continue-on-error: true 16 | steps: 17 | # Checks out a copy of your repository on the ubuntu-latest machine 18 | - name: Checkout code 19 | uses: actions/checkout@v2 20 | 21 | # Runs the Super-Linter action 22 | - name: Run Super-Linter 23 | uses: github/super-linter@v4 24 | env: 25 | DEFAULT_BRANCH: master 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | FILTER_REGEX_EXCLUDE: .*.css|.*.js 28 | DISABLE_ERRORS: true 29 | 30 | -------------------------------------------------------------------------------- /donkeycar/tests/test_controller.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from .setup import on_pi 3 | from donkeycar.parts.controller import PS3Joystick, PS3JoystickController 4 | 5 | 6 | def test_ps3_joystick(): 7 | js = PS3Joystick() 8 | assert js is not None 9 | js.init() 10 | 11 | def test_ps3_joystick_controller(): 12 | js = PS3JoystickController() 13 | assert js is not None 14 | js.init_js() 15 | js.run_threaded(None) 16 | js.print_controls() 17 | def test_fn(): 18 | pass 19 | js.set_button_down_trigger("x", test_fn) 20 | js.erase_last_N_records() 21 | js.on_throttle_changes() 22 | js.emergency_stop() 23 | #js.update() 24 | js.set_steering(0.0) 25 | js.set_throttle(0.0) 26 | js.toggle_manual_recording() 27 | js.increase_max_throttle() 28 | js.decrease_max_throttle() 29 | js.toggle_constant_throttle() 30 | js.toggle_mode() 31 | js.chaos_monkey_on_left() 32 | js.chaos_monkey_on_right() 33 | js.chaos_monkey_off() -------------------------------------------------------------------------------- /donkeycar/geom.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Geometry 3 | Author: Tawn Kramer 4 | Date: Nov 11, 2014 5 | ''' 6 | from .la import Vec2 7 | 8 | class LineSeg2d(object): 9 | 10 | def __init__(self, x1, y1, x2, y2): 11 | a = Vec2(x1, y1) 12 | b = Vec2(x2, y2) 13 | self.point = a 14 | self.end = b 15 | self.ray = a - b 16 | self.ray.normalize() 17 | 18 | def closest_vec_to(self, vec2_pt): 19 | ''' 20 | produces a vector normal to this line passing through the given point vec2_pt 21 | ''' 22 | delta_pt = self.point - vec2_pt 23 | dp = delta_pt.dot(self.ray) 24 | return self.ray * dp - delta_pt 25 | 26 | def cross_track_error(self, vec2_pt): 27 | ''' 28 | a signed magnitude of distance from line segment 29 | ''' 30 | err_vec = self.closest_vec_to(vec2_pt) 31 | mag = err_vec.mag() 32 | err_vec.scale(1.0 / mag) 33 | sign = 1. 34 | if err_vec.cross(self.ray) < 0.0: 35 | sign = -1. 36 | return mag * sign 37 | 38 | -------------------------------------------------------------------------------- /donkeycar/benchmarks/tub_v2.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import timeit 4 | from pathlib import Path 5 | 6 | import numpy as np 7 | 8 | from donkeycar.parts.tub_v2 import Tub 9 | 10 | 11 | def benchmark(): 12 | # Change to a non SSD storage path 13 | path = Path('/media/rahulrav/Cruzer/benchmark') 14 | 15 | # Recreate paths 16 | if os.path.exists(path.absolute().as_posix()): 17 | shutil.rmtree(path) 18 | 19 | inputs = ['input'] 20 | types = ['int'] 21 | tub = Tub(path.as_posix(), inputs, types, max_catalog_len=1000) 22 | write_count = 1000 23 | for i in range(write_count): 24 | record = {'input': i} 25 | tub.write_record(record) 26 | 27 | deletions = np.random.randint(0, write_count, 100) 28 | tub.delete_records(deletions) 29 | 30 | for record in tub: 31 | print('Record %s' % record) 32 | 33 | tub.close() 34 | 35 | 36 | if __name__ == "__main__": 37 | timer = timeit.Timer(benchmark) 38 | time_taken = timer.timeit(number=1) 39 | print('Time taken %s seconds' % time_taken) 40 | print('\nDone.') 41 | -------------------------------------------------------------------------------- /.github/workflows/python-package-conda.yml: -------------------------------------------------------------------------------- 1 | name: Python package and test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | checkout-test: 9 | name: Checkout and test 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | os: ["macos-latest", "ubuntu-latest"] 14 | fail-fast: false 15 | defaults: 16 | run: 17 | shell: bash -l {0} 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v4 21 | - name: Create python 3.11 conda env 22 | uses: conda-incubator/setup-miniconda@v3 23 | with: 24 | python-version: 3.11 25 | mamba-version: "*" 26 | activate-environment: donkey 27 | auto-activate-base: false 28 | channels: default, conda-forge, pytorch 29 | channel-priority: true 30 | - name: Conda info and list 31 | run: | 32 | conda info 33 | conda list 34 | - name: Install donkey 35 | run: | 36 | pip install -e .[pc,dev] 37 | pip list 38 | - name: Run tests 39 | run: pytest 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Will Roscoe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /donkeycar/templates/calibration_odometry.json: -------------------------------------------------------------------------------- 1 | { 2 | "velocimeters": [ 3 | { 4 | "scale_and_alignment": [ 5 | 1.0, 6 | 0.0, 7 | 0.0, 8 | 0.0, 9 | 1.0, 10 | 0.0, 11 | 0.0, 12 | 0.0, 13 | 1.0 14 | ], 15 | "noise_variance": 0.001, 16 | "extrinsics": { 17 | "T": [ 18 | 0.0, 19 | -0.1, 20 | 0.1 21 | ], 22 | "T_variance": [ 23 | 9.999999974752427e-7, 24 | 9.999999974752427e-7, 25 | 9.999999974752427e-7 26 | ], 27 | "W": [ 28 | -1.1155, 29 | -1.1690, 30 | -1.2115 31 | ], 32 | "W_variance": [ 33 | 9.999999974752427e-5, 34 | 9.999999974752427e-5, 35 | 9.999999974752427e-5 36 | ] 37 | } 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /scripts/profile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Script to drive a TF model as fast as possible 4 | 5 | Usage: 6 | profile.py (--model=) (--type=) 7 | 8 | Options: 9 | -h --help Show this screen. 10 | """ 11 | import os 12 | from docopt import docopt 13 | import donkeycar as dk 14 | import numpy as np 15 | from donkeycar.utils import FPSTimer 16 | 17 | 18 | def profile(model_path, model_type): 19 | cfg = dk.load_config('config.py') 20 | model_path = os.path.expanduser(model_path) 21 | model = dk.utils.get_model_by_type(model_type, cfg) 22 | model.load(model_path) 23 | 24 | h, w, ch = cfg.IMAGE_H, cfg.IMAGE_W, cfg.IMAGE_DEPTH 25 | 26 | # generate random array in the right shape in [0,1) 27 | img = np.random.randint(0, 255, size=(h, w, ch)) 28 | 29 | # make a timer obj 30 | timer = FPSTimer() 31 | 32 | try: 33 | while True: 34 | # run inferencing 35 | model.run(img) 36 | # time 37 | timer.on_frame() 38 | 39 | except KeyboardInterrupt: 40 | pass 41 | 42 | 43 | if __name__ == '__main__': 44 | args = docopt(__doc__) 45 | profile(model_path=args['--model'], model_type=args['--type']) 46 | -------------------------------------------------------------------------------- /donkeycar/parts/pytorch/torch_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def get_model_by_type(model_type, cfg, checkpoint_path=None): 5 | ''' 6 | given the string model_type and the configuration settings in cfg 7 | create a Torch model and return it. 8 | ''' 9 | if model_type is None: 10 | model_type = cfg.DEFAULT_MODEL_TYPE 11 | print("\"get_model_by_type\" model Type is: {}".format(model_type)) 12 | 13 | input_shape = (cfg.BATCH_SIZE, cfg.IMAGE_DEPTH, cfg.IMAGE_H, cfg.IMAGE_W) 14 | 15 | if model_type == "resnet18": 16 | from donkeycar.parts.pytorch.ResNet18 import ResNet18 17 | # ResNet18 will always use the following input size 18 | # regardless of what the user specifies. This is necessary since 19 | # the model is pre-trained on ImageNet 20 | input_shape = (cfg.BATCH_SIZE, 3, 224, 224) 21 | model = ResNet18(input_shape=input_shape) 22 | else: 23 | raise Exception("Unknown model type {:}, supported types are " 24 | "resnet18" 25 | .format(model_type)) 26 | 27 | if checkpoint_path: 28 | print("Loading model from checkpoint {}".format(checkpoint_path)) 29 | model.load_from_checkpoint(checkpoint_path) 30 | 31 | return model 32 | -------------------------------------------------------------------------------- /donkeycar/gym/remote_controller.py: -------------------------------------------------------------------------------- 1 | ''' 2 | file: remote_controller.py 3 | author: Tawn Kramer 4 | date: 2019-01-24 5 | desc: Control a remote donkey robot over network 6 | ''' 7 | 8 | import time 9 | 10 | from donkeycar.parts.network import MQTTValueSub, MQTTValuePub 11 | from donkeycar.parts.image import JpgToImgArr 12 | 13 | class DonkeyRemoteContoller: 14 | def __init__(self, donkey_name, mqtt_broker, sensor_size=(120, 160, 3)): 15 | self.camera_sub = MQTTValueSub("donkey/%s/camera" % donkey_name, broker=mqtt_broker) 16 | self.controller_pub = MQTTValuePub("donkey/%s/controls" % donkey_name, broker=mqtt_broker) 17 | self.jpgToImg = JpgToImgArr() 18 | self.sensor_size = sensor_size 19 | 20 | def get_sensor_size(self): 21 | return self.sensor_size 22 | 23 | def wait_until_connected(self): 24 | pass 25 | 26 | def take_action(self, action): 27 | self.controller_pub.run(action) 28 | 29 | def quit(self): 30 | self.camera_sub.shutdown() 31 | self.controller_pub.shutdown() 32 | 33 | def get_original_image(self): 34 | return self.img 35 | 36 | def observe(self): 37 | jpg = self.camera_sub.run() 38 | self.img = self.jpgToImg.run(jpg) 39 | return self.img 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /donkeycar/tests/test_template.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from tempfile import gettempdir 4 | from donkeycar.templates import complete 5 | import donkeycar as dk 6 | import os 7 | 8 | from .setup import default_template, d2_path, custom_template 9 | 10 | 11 | def test_config(): 12 | path = default_template(d2_path(gettempdir())) 13 | cfg = dk.load_config(os.path.join(path, 'config.py')) 14 | assert (cfg is not None) 15 | 16 | 17 | def test_drive(): 18 | path = default_template(d2_path(gettempdir())) 19 | myconfig = open(os.path.join(path, 'myconfig.py'), "wt") 20 | myconfig.write("CAMERA_TYPE = 'MOCK'\n") 21 | myconfig.write("USE_SSD1306_128_32 = False \n") 22 | myconfig.write("DRIVE_TRAIN_TYPE = 'None'") 23 | myconfig.close() 24 | cfg = dk.load_config(os.path.join(path, 'config.py')) 25 | cfg.MAX_LOOPS = 10 26 | complete.drive(cfg=cfg) 27 | 28 | 29 | def test_custom_templates(): 30 | template_names = ["complete", "basic", "square"] 31 | for template in template_names: 32 | path = custom_template(d2_path(gettempdir()), template=template) 33 | cfg = dk.load_config(os.path.join(path, 'config.py')) 34 | assert (cfg is not None) 35 | mcfg = dk.load_config(os.path.join(path, 'myconfig.py')) 36 | assert (mcfg is not None) 37 | -------------------------------------------------------------------------------- /donkeycar/benchmarks/tub.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import shutil 4 | import timeit 5 | from pathlib import Path 6 | 7 | import numpy as np 8 | 9 | from donkeycar.parts.datastore import Tub 10 | 11 | 12 | def benchmark(): 13 | # Change with a non SSD storage path 14 | path = Path('/media/rahulrav/Cruzer/tub') 15 | if os.path.exists(path.absolute().as_posix()): 16 | shutil.rmtree(path) 17 | 18 | inputs = ['input'] 19 | types = ['int'] 20 | tub = Tub(path.absolute().as_posix(), inputs, types) 21 | write_count = 1000 22 | for i in range(write_count): 23 | record = {'input': i} 24 | tub.put_record(record) 25 | 26 | # old tub starts counting at 1 27 | deletions = set(np.random.randint(1, write_count + 1, 100)) 28 | for index in deletions: 29 | index = int(index) 30 | tub.remove_record(index) 31 | 32 | files = path.glob('*.json') 33 | for record_file in files: 34 | contents = record_file.read_text() 35 | if contents: 36 | contents = json.loads(contents) 37 | print('Record %s' % contents) 38 | 39 | 40 | if __name__ == "__main__": 41 | timer = timeit.Timer(benchmark) 42 | time_taken = timer.timeit(number=1) 43 | print('Time taken %s seconds' % time_taken) 44 | print('\nDone.') 45 | -------------------------------------------------------------------------------- /donkeycar/parts/graph.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cv2 3 | 4 | 5 | class Graph(object): 6 | ''' 7 | Take input values and plot them on an image. 8 | Takes a list of (x, y) (b, g, r) pairs and 9 | plots the color at the given coordinate. 10 | When the x value exceeds the width, the graph is erased 11 | and begins with an offset to x values such that drawing 12 | begins again at the left edge. 13 | This assumes x is monotonically increasing, like a time value. 14 | ''' 15 | def __init__(self, res=(200, 200, 3)): 16 | self.img = np.zeros(res) 17 | self.prev = 0 18 | 19 | def clamp(self, val, lo, hi): 20 | if val < lo: 21 | val = lo 22 | elif val > hi: 23 | val = hi 24 | return int(val) 25 | 26 | def run(self, values): 27 | if values is None: 28 | return self.img 29 | 30 | for coord, col in values: 31 | x = coord[0] % self.img.shape[1] 32 | y = self.clamp(coord[1], 0, self.img.shape[0] - 1) 33 | self.img[y, x] = col 34 | 35 | if abs(self.prev - x) > self.img.shape[1] / 2: 36 | self.img = np.zeros_like(self.img) 37 | 38 | self.prev = x 39 | 40 | return self.img 41 | 42 | def shutdown(self): 43 | pass 44 | -------------------------------------------------------------------------------- /donkeycar/management/tub_web/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tub Manager 6 | 7 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 41 | 42 | {% block content %}{% end %} 43 | 44 | 45 | -------------------------------------------------------------------------------- /donkeycar/parts/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import List, Set, Dict, Tuple, Optional 3 | 4 | 5 | class LoggerPart: 6 | """ 7 | Log the given values in vehicle memory. 8 | """ 9 | def __init__(self, inputs: List[str], level: str="INFO", rate: int=1, logger=None): 10 | self.inputs = inputs 11 | self.rate = rate 12 | self.level = logging._nameToLevel.get(level, logging.INFO) 13 | self.logger = logging.getLogger(logger if logger is not None else "LoggerPart") 14 | 15 | self.values = {} 16 | self.count = 0 17 | self.running = True 18 | 19 | def run(self, *args): 20 | if self.running and args is not None and len(args) == len(self.inputs): 21 | self.count = (self.count + 1) % (self.rate + 1) 22 | for i in range(len(self.inputs)): 23 | field = self.inputs[i] 24 | value = args[i] 25 | old_value = self.values.get(field) 26 | if old_value != value: 27 | # always log changes 28 | self.logger.log(self.level, f"{field} = {old_value} -> {value}") 29 | self.values[field] = value 30 | elif self.count >= self.rate: 31 | self.logger.log(self.level, f"{field} = {value}") 32 | 33 | def shutdown(self): 34 | self.running = False 35 | -------------------------------------------------------------------------------- /scripts/tflite_profile.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Usage: 3 | tflite_test.py --model="mymodel.tflite" 4 | 5 | Note: 6 | may require tensorflow > 1.11 or 7 | pip install tf-nightly 8 | ''' 9 | import os 10 | 11 | from docopt import docopt 12 | import tensorflow as tf 13 | import numpy as np 14 | 15 | from donkeycar.utils import FPSTimer 16 | 17 | args = docopt(__doc__) 18 | 19 | in_model = os.path.expanduser(args['--model']) 20 | 21 | # Load TFLite model and allocate tensors. 22 | interpreter = tf.lite.Interpreter(model_path=in_model) 23 | interpreter.allocate_tensors() 24 | 25 | # Get input and output tensors. 26 | input_details = interpreter.get_input_details() 27 | output_details = interpreter.get_output_details() 28 | 29 | # Test model on random input data. 30 | input_shape = input_details[0]['shape'] 31 | input_data = np.array(np.random.random_sample(input_shape), dtype=np.float32) 32 | 33 | interpreter.set_tensor(input_details[0]['index'], input_data) 34 | interpreter.invoke() 35 | 36 | #sample output 37 | for tensor in output_details: 38 | output_data = interpreter.get_tensor(tensor['index']) 39 | print(output_data) 40 | 41 | #run in a loop to test performance. 42 | print("test performance: hit CTRL+C to break") 43 | timer = FPSTimer() 44 | while True: 45 | interpreter.set_tensor(input_details[0]['index'], input_data) 46 | interpreter.invoke() 47 | timer.on_frame() 48 | 49 | -------------------------------------------------------------------------------- /donkeycar/contrib/robohat/rear_light.py: -------------------------------------------------------------------------------- 1 | import neopixel 2 | import board 3 | import adafruit_logging as logging 4 | logger = logging.getLogger('rear_light') 5 | logger.setLevel(logging.INFO) 6 | 7 | 8 | class RearLight: 9 | """ 10 | Class for controlling rear light 11 | """ 12 | RED = (255, 0, 0) 13 | DARK = (0, 0, 0) 14 | YELLOW = (255, 150, 0) 15 | 16 | def __init__(self): 17 | pixel_pin = board.NEOPIXEL 18 | num_pixels = 16 19 | pixel_brightness = 0.4 20 | self.pixels = neopixel.NeoPixel(pixel_pin, num_pixels, brightness=pixel_brightness) 21 | 22 | self.is_brake_light_on = False 23 | 24 | def fill_pixels(self, color): 25 | self.pixels.fill(color) 26 | self.pixels.show() 27 | 28 | def turn_on_brake_light(self): 29 | if not self.is_brake_light_on: 30 | logger.info("turning on brake light") 31 | self.fill_pixels(RearLight.RED) 32 | self.is_brake_light_on = True 33 | 34 | def turn_off_brake_light(self): 35 | if self.is_brake_light_on: 36 | logger.info("turning off brake light") 37 | self.fill_pixels(RearLight.DARK) 38 | self.is_brake_light_on = False 39 | 40 | def run(self, angle, throttle): 41 | if throttle > 1600: 42 | self.turn_off_brake_light() 43 | else: 44 | self.turn_on_brake_light() 45 | -------------------------------------------------------------------------------- /donkeycar/tests/test_catalog_v2.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tempfile 4 | import time 5 | import unittest 6 | from pathlib import Path 7 | 8 | from donkeycar.parts.datastore_v2 import Catalog, CatalogMetadata, Seekable 9 | 10 | 11 | class TestCatalog(unittest.TestCase): 12 | 13 | def setUp(self): 14 | self._path = tempfile.mkdtemp() 15 | self._catalog_path = os.path.join(self._path, 'test.catalog') 16 | 17 | def test_basic_catalog_operations(self): 18 | catalog = Catalog(self._catalog_path) 19 | for i in range(0, 10): 20 | catalog.write_record(self._newRecord()) 21 | 22 | self.assertEqual(os.path.exists(catalog.path.as_posix()), True) 23 | self.assertEqual(os.path.exists(catalog.manifest.manifest_path.as_posix()), True) 24 | 25 | catalog_2 = Catalog(self._catalog_path) 26 | catalog_2.seekable.seek_line_start(1) 27 | line = catalog_2.seekable.readline() 28 | count = 0 29 | while line is not None and len(line) > 0: 30 | print('Contents %s' % (line)) 31 | count += 1 32 | line = catalog_2.seekable.readline() 33 | 34 | self.assertEqual(count, 10) 35 | 36 | def tearDown(self): 37 | shutil.rmtree(self._path) 38 | 39 | def _newRecord(self): 40 | record = {'at' : time.time()} 41 | return record 42 | 43 | if __name__ == '__main__': 44 | unittest.main() 45 | -------------------------------------------------------------------------------- /donkeycar/parts/ros.py: -------------------------------------------------------------------------------- 1 | import rospy 2 | from std_msgs.msg import String, Int32, Float32 3 | 4 | ''' 5 | sudo apt-get install python3-catkin-pkg 6 | 7 | ROS issues w python3: 8 | https://discourse.ros.org/t/should-we-warn-new-users-about-difficulties-with-python-3-and-alternative-python-interpreters/3874/3 9 | ''' 10 | 11 | class RosPubisher(object): 12 | ''' 13 | A ROS node to pubish to a data stream 14 | ''' 15 | def __init__(self, node_name, channel_name, stream_type=String, anonymous=True): 16 | self.data = "" 17 | self.pub = rospy.Publisher(channel_name, stream_type) 18 | rospy.init_node(node_name, anonymous=anonymous) 19 | 20 | def run(self, data): 21 | ''' 22 | only publish when data stream changes. 23 | ''' 24 | if data != self.data and not rospy.is_shutdown(): 25 | self.data = data 26 | self.pub.publish(data) 27 | 28 | 29 | class RosSubscriber(object): 30 | ''' 31 | A ROS node to subscribe to a data stream 32 | ''' 33 | 34 | def __init__(self, node_name, channel_name, stream_type=String, anonymous=True): 35 | self.data = "" 36 | rospy.init_node(node_name, anonymous=anonymous) 37 | self.pub = rospy.Subscriber(channel_name, stream_type, self.on_data_recv) 38 | 39 | def on_data_recv(self, data): 40 | self.data = data.data 41 | 42 | def run(self): 43 | return self.data 44 | 45 | -------------------------------------------------------------------------------- /donkeycar/tests/test_tubwriter.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import tempfile 3 | import unittest 4 | from random import randint 5 | 6 | from donkeycar.parts.tub_v2 import Tub, TubWriter 7 | 8 | 9 | class TestTub(unittest.TestCase): 10 | def setUp(self): 11 | self._path = tempfile.mkdtemp() 12 | 13 | def test_tubwriter_sessions(self): 14 | # run tubwriter multiple times on the same tub directory 15 | write_counts = [] 16 | for _ in range(5): 17 | tub_writer = TubWriter(self._path, inputs=['input'], types=['int']) 18 | write_count = randint(1, 10) 19 | for i in range(write_count): 20 | tub_writer.run(i) 21 | tub_writer.close() 22 | write_counts.append(write_count) 23 | 24 | # Check we have good session id for all new records: 25 | id = 0 26 | total = 0 27 | for record in tub_writer.tub: 28 | print(f'Record: {record}') 29 | session_number = int(record['_session_id'].split('_')[1]) 30 | self.assertEqual(session_number, id, 31 | 'Session id not correctly generated') 32 | total += 1 33 | if total == write_counts[0]: 34 | total = 0 35 | id += 1 36 | write_counts.pop(0) 37 | 38 | def tearDown(self): 39 | shutil.rmtree(self._path) 40 | 41 | 42 | if __name__ == '__main__': 43 | unittest.main() 44 | -------------------------------------------------------------------------------- /scripts/salient_vis_listener.py: -------------------------------------------------------------------------------- 1 | """ 2 | Scripts to drive a donkey 2 car 3 | 4 | Usage: 5 | salient_vis_listener.py [--ip="localhost"] [--model=] [--type=(linear|categorical|rnn|imu|behavior|3d|localizer)] [--config="config.py"] 6 | 7 | 8 | Options: 9 | -h --help Show this screen. 10 | """ 11 | import os 12 | import time 13 | import math 14 | from docopt import docopt 15 | import donkeycar as dk 16 | 17 | from donkeycar.parts.cv import CvImageView, ImgBGR2RGB, ImgRGB2BGR, ImageScale, ImgWriter 18 | from donkeycar.parts.salient import SalientVis 19 | from donkeycar.parts.network import ZMQValueSub, UDPValueSub, TCPClientValue 20 | from donkeycar.parts.transform import Lambda 21 | from donkeycar.parts.image import JpgToImgArr 22 | 23 | V = dk.vehicle.Vehicle() 24 | args = docopt(__doc__) 25 | cfg = dk.load_config(args['--config']) 26 | 27 | model_path = args['--model'] 28 | model_type = args['--type'] 29 | ip = args['--ip'] 30 | 31 | if model_type is None: 32 | model_type = "categorical" 33 | 34 | model = dk.utils.get_model_by_type(model_type, cfg) 35 | model.load(model_path) 36 | 37 | V.add(TCPClientValue(name="camera", host=ip), outputs=["packet"]) 38 | V.add(JpgToImgArr(), inputs=["packet"], outputs=["img"]) 39 | V.add(ImgBGR2RGB(), inputs=["img"], outputs=["img"]) 40 | V.add(SalientVis(model), inputs=["img"], outputs=["img"]) 41 | V.add(ImageScale(4.0), inputs=["img"], outputs=["lg_img"]) 42 | V.add(CvImageView(), inputs=["lg_img"]) 43 | 44 | V.start(rate_hz=1) 45 | 46 | -------------------------------------------------------------------------------- /donkeycar/parts/web_controller/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Donkey Monitor 6 | 7 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 44 | 45 | {% block content %}{% end %} 46 | 47 | 48 | -------------------------------------------------------------------------------- /donkeycar/parts/fps.py: -------------------------------------------------------------------------------- 1 | import time 2 | import logging 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | 7 | class FrequencyLogger(object): 8 | """ 9 | Log current, min and max of frequency value 10 | """ 11 | 12 | def __init__(self, debug_interval=10): 13 | self.last_timestamp = None 14 | self.counter = 0 15 | self.fps = 0 16 | self.fps_list = [] 17 | 18 | self.last_debug_timestamp = None 19 | self.debug_interval = debug_interval 20 | 21 | def run(self): 22 | if self.last_timestamp is None: 23 | self.last_timestamp = time.time() 24 | 25 | if time.time() - self.last_timestamp > 1: 26 | self.fps = self.counter 27 | self.fps_list.append(self.counter) 28 | self.counter = 0 29 | self.last_timestamp = time.time() 30 | else: 31 | self.counter += 1 32 | 33 | # Printing frequency info into shell 34 | if self.last_debug_timestamp is None: 35 | self.last_debug_timestamp = time.time() 36 | 37 | if time.time() - self.last_debug_timestamp > self.debug_interval: 38 | logger.info(f"current fps = {self.fps}") 39 | self.last_debug_timestamp = time.time() 40 | 41 | return self.fps, self.fps_list 42 | 43 | def shutdown(self): 44 | if self.fps_list: 45 | logger.info(f"fps (min/max) = {min(self.fps_list):2d} / {max(self.fps_list):2d}") 46 | logger.info(f"fps list = {self.fps_list}".format()) 47 | -------------------------------------------------------------------------------- /donkeycar/tests/test_odometer.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | import time 4 | 5 | from donkeycar.parts.odometer import Odometer 6 | 7 | class TestOdometer(unittest.TestCase): 8 | 9 | def test_odometer(self): 10 | odometer = Odometer(0.2) # 0.2 meters per revolution 11 | 12 | ts = time.time() # initial time 13 | distance, velocity, timestamp = odometer.run(1, ts) # first reading is one revolution 14 | self.assertEqual(ts, timestamp) 15 | self.assertEqual(0.2, distance) # distance travelled is 0.2 meters 16 | self.assertEqual(0, velocity) # zero velocity until we get two distances 17 | 18 | ts += 1 # add one second 19 | distance, velocity, timestamp = odometer.run(2, ts) # total of 2 revolutions 20 | self.assertEqual(ts, timestamp) 21 | self.assertEqual(0.4, distance) # 0.4 meters travelled 22 | self.assertEqual(0.2, velocity) # 0.2 meters per second since last update 23 | 24 | ts += 1 # add one second 25 | distance, velocity, timestamp = odometer.run(2, ts) # don't move 26 | self.assertEqual(ts, timestamp) 27 | self.assertEqual(0.4, distance) # still at 0.4 meters travelled 28 | self.assertEqual(0, velocity) # 0 meter per second in last interval 29 | 30 | -------------------------------------------------------------------------------- /scripts/graph_listener.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import time 4 | import math 5 | from docopt import docopt 6 | import donkeycar as dk 7 | 8 | from donkeycar.parts.cv import CvImageView 9 | from donkeycar.parts.graph import Graph 10 | from donkeycar.parts.network import ZMQValueSub 11 | from donkeycar.parts.transform import Lambda 12 | 13 | V = dk.vehicle.Vehicle() 14 | ip = "localhost" 15 | w = 640 16 | h = 480 17 | d = 3 18 | 19 | def condition_values(obj): 20 | if obj is None: 21 | return None 22 | ''' 23 | This expects a tuple of 4 values. 24 | The first value is time (x), and the rest are y values 25 | from -2, +2 26 | This will work with the network publisher test: 27 | python ~/projects/donkey_tkramer/donkeycar/parts/network.py 28 | ''' 29 | 30 | vals = obj[1:] 31 | x = round(obj[0] * 30.0) 32 | ret = [] 33 | 34 | i = 0 35 | for val in vals: 36 | coord = (x, val * (h / 4.) + (h / 2.)) 37 | color = [0, 0, 0] 38 | color[i] = 1 39 | i += 1 40 | ret.append( (coord, color) ) 41 | 42 | #a solid white center line. 43 | coord = (x, h / 2.0) 44 | color = (1.0, 1.0, 1.0) 45 | ret.append( (coord, color) ) 46 | 47 | return ret 48 | 49 | l = Lambda(condition_values) 50 | 51 | V.add(ZMQValueSub(name="test", ip=ip), outputs=["obj"]) 52 | V.add(l, inputs=["obj"], outputs=["values"]) 53 | V.add(Graph(res=(h, w, d)), inputs=["values"], outputs=["graph/img"]) 54 | V.add(CvImageView(), inputs=["graph/img"]) 55 | 56 | V.start(rate_hz=10) 57 | -------------------------------------------------------------------------------- /scripts/remote_cam_view_tcp.py: -------------------------------------------------------------------------------- 1 | """ 2 | Script to view a donkeycar camera remotely (when published using TcpServer) 3 | 4 | Usage: 5 | remote_cam_view_tcp.py (--ip=) [--record=] 6 | 7 | Options: 8 | -h --help Show this screen. 9 | --record= If data should be recorded (locally) specify the path 10 | 11 | """ 12 | from docopt import docopt 13 | import donkeycar as dk 14 | from donkeycar.parts.cv import CvImageView, ImgBGR2RGB, ImageScale 15 | from donkeycar.parts.network import TCPClientValue 16 | from donkeycar.parts.image import JpgToImgArr 17 | 18 | args = docopt(__doc__) 19 | print(args) 20 | 21 | V = dk.vehicle.Vehicle() 22 | V.add(TCPClientValue("camera", args["--ip"]), outputs=["jpg"]) 23 | V.add(JpgToImgArr(), inputs=["jpg"], outputs=["img_arr"]) 24 | V.add(ImgBGR2RGB(), inputs=["img_arr"], outputs=["rgb"]) 25 | V.add(ImageScale(4.0), inputs=["rgb"], outputs=["lg_img"]) 26 | V.add(CvImageView(), inputs=["lg_img"]) 27 | 28 | # Local saving of images? 29 | record_path = args["--record"] 30 | if record_path is not None: 31 | class ImageSaver: 32 | def __init__(self, path): 33 | self.index = 0 34 | self.path = path 35 | 36 | def run(self, img_arr): 37 | if img_arr is None: 38 | return 39 | dest_path = os.path.join(self.path, "img_%d.jpg" % self.index) 40 | self.index += 1 41 | cv2.imwrite(dest_path, img_arr) 42 | 43 | V.add(ImageSaver(record_path), inputs=["rgb"]) 44 | 45 | V.start(rate_hz=20) 46 | -------------------------------------------------------------------------------- /scripts/convert_to_tflite.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Usage: 4 | convert_to_tflite.py [-o | --overwrite] ... 5 | 6 | Options: 7 | -h --help Show this screen. 8 | -o --overwrite Force overwriting existing TFLite files 9 | 10 | Note: 11 | This script converts a keras (.h5) or tensorflow (.savedmodel) into 12 | TFlite. Supports multiple input files. 13 | 14 | """ 15 | 16 | from docopt import docopt 17 | from donkeycar.parts.interpreter import keras_model_to_tflite 18 | from os.path import splitext, exists 19 | 20 | if __name__ == '__main__': 21 | args = docopt(__doc__) 22 | model_path_list = args[''] 23 | overwrite = args['-o'] or args['--overwrite'] 24 | print(f"Found {len(model_path_list)} models to process.") 25 | print(f"Overwrite set to: {overwrite}.") 26 | count = 0 27 | for model_path in model_path_list: 28 | base_path, ext = splitext(model_path) 29 | if ext not in ['.h5', '.savedmodel']: 30 | print(f"Can only convert '.h5' or '.savedmodel' but not {ext}") 31 | continue 32 | tflite_filename = base_path + '.tflite' 33 | if exists(tflite_filename) and not overwrite: 34 | print(f"Found existing tflite mode {tflite_filename}, will skip. " 35 | f"If you want to overwrite existing files, please specify " 36 | f"the option --overwrite or -o ") 37 | continue 38 | keras_model_to_tflite(model_path, tflite_filename) 39 | count += 1 40 | print(f"Finished converting {count} models") 41 | -------------------------------------------------------------------------------- /scripts/preview_augumentations.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Usage: 4 | preview_augmentations.py 5 | 6 | Note: 7 | This script helps preview augmentations used when the model is being trained. 8 | """ 9 | 10 | import time 11 | import cv2 12 | 13 | from donkeycar.pipeline.augmentations import Augmentations 14 | 15 | # Camera Parameters 16 | WIDTH = 640 17 | HEIGHT = 480 18 | 19 | # Example augumentations 20 | cropping = Augmentations.crop(0, 0, 100, 0, keep_size=True) 21 | mask = Augmentations.trapezoidal_mask(10, 630, 100, 300, 50, 480) 22 | 23 | 24 | def preview_augmentations(): 25 | print('Connecting to Camera') 26 | capture = cv2.VideoCapture(0) 27 | time.sleep(2) 28 | if capture.isOpened(): 29 | print('Camera Connected.') 30 | else: 31 | print('Unable to connect. Are you sure you are using the right camera parameters ?') 32 | return 33 | 34 | while True: 35 | success, frame = capture.read() 36 | if success: 37 | cropped = cropping.augment_image(frame) 38 | masked = mask.augment_image(frame) 39 | # Convert to RGB 40 | cv2.imshow('Preview', frame) 41 | cv2.imshow('Cropped', cropped) 42 | cv2.imshow('Trapezoidal Mask', masked) 43 | prompt = cv2.waitKey(1) & 0xFF 44 | if prompt == ord(' '): 45 | # Store output 46 | pass 47 | elif prompt == ord('q'): 48 | break 49 | 50 | capture.release() 51 | cv2.destroyAllWindows() 52 | 53 | 54 | if __name__ == "__main__": 55 | preview_augmentations() 56 | -------------------------------------------------------------------------------- /donkeycar/parts/launch.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | class AiLaunch(): 4 | ''' 5 | This part will apply a large thrust on initial activation. This is to help 6 | in racing to start fast and then the ai will take over quickly when it's 7 | up to speed. 8 | ''' 9 | 10 | def __init__(self, launch_duration=1.0, launch_throttle=1.0, keep_enabled=False): 11 | self.active = False 12 | self.enabled = False 13 | self.timer_start = None 14 | self.timer_duration = launch_duration 15 | self.launch_throttle = launch_throttle 16 | self.prev_mode = None 17 | self.trigger_on_switch = keep_enabled 18 | 19 | def enable_ai_launch(self): 20 | self.enabled = True 21 | print('AiLauncher is enabled.') 22 | 23 | def run(self, mode, ai_throttle): 24 | new_throttle = ai_throttle 25 | 26 | if mode != self.prev_mode: 27 | self.prev_mode = mode 28 | if mode == "local" and self.trigger_on_switch: 29 | self.enabled = True 30 | 31 | if mode == "local" and self.enabled: 32 | if not self.active: 33 | self.active = True 34 | self.timer_start = time.time() 35 | else: 36 | duration = time.time() - self.timer_start 37 | if duration > self.timer_duration: 38 | self.active = False 39 | self.enabled = False 40 | else: 41 | self.active = False 42 | 43 | if self.active: 44 | print('AiLauncher is active!!!') 45 | new_throttle = self.launch_throttle 46 | 47 | return new_throttle 48 | 49 | -------------------------------------------------------------------------------- /donkeycar/parts/perfmon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Performance monitor for analyzing real-time CPU/mem/execution frequency 5 | 6 | author: @miro (Meir Tseitlin) 2020 7 | 8 | Note: 9 | """ 10 | import time 11 | import psutil 12 | 13 | 14 | class PerfMonitor: 15 | 16 | def __init__(self, cfg): 17 | 18 | self.STATS_BUFFER_SIZE = 10 19 | self._calc_buffer = [cfg.DRIVE_LOOP_HZ for i in range(self.STATS_BUFFER_SIZE)] 20 | self._runs_counter = 0 21 | self._last_calc_time = time.time() 22 | self._on = True 23 | self._update_metrics() 24 | print("Performance monitor activated.") 25 | 26 | def _update_metrics(self): 27 | self._mem_percent = psutil.virtual_memory().percent 28 | self._cpu_percent = psutil.cpu_percent() 29 | 30 | def update(self): 31 | while self._on: 32 | self._update_metrics() 33 | time.sleep(2) 34 | 35 | def shutdown(self): 36 | # indicate that the thread should be stopped 37 | self._on = False 38 | print('Stopping Perf Monitor') 39 | time.sleep(.2) 40 | 41 | def run_threaded(self): 42 | 43 | # Calc real frequency 44 | curr_time = time.time() 45 | if curr_time - self._last_calc_time > 1: 46 | self._calc_buffer[int(curr_time) % self.STATS_BUFFER_SIZE] = self._runs_counter 47 | self._runs_counter = 0 48 | self._last_calc_time = curr_time 49 | 50 | self._runs_counter += 1 51 | 52 | vehicle_frequency = float(sum(self._calc_buffer)) / self.STATS_BUFFER_SIZE 53 | 54 | return self._cpu_percent, self._mem_percent, vehicle_frequency 55 | -------------------------------------------------------------------------------- /donkeycar/tests/test_vehicle.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import donkeycar as dk 3 | from donkeycar.parts.transform import Lambda 4 | 5 | 6 | def _get_sample_lambda(): 7 | def f(): 8 | return 1 9 | f.update = f 10 | return Lambda(f) 11 | 12 | 13 | @pytest.fixture() 14 | def vehicle(): 15 | v = dk.Vehicle() 16 | v.add(_get_sample_lambda(), outputs=['test_out']) 17 | return v 18 | 19 | 20 | def test_create_vehicle(): 21 | v = dk.Vehicle() 22 | assert v.parts == [] 23 | 24 | 25 | def test_add_part(): 26 | v = dk.Vehicle() 27 | v.add(_get_sample_lambda(), outputs=['test_out']) 28 | assert len(v.parts) == 1 29 | 30 | 31 | def test_vehicle_run(vehicle): 32 | vehicle.start(rate_hz=20, max_loop_count=2) 33 | assert vehicle is not None 34 | 35 | 36 | def test_should_raise_assertion_on_non_list_inputs_for_add_part(): 37 | vehicle = dk.Vehicle() 38 | inputs = 'any' 39 | with pytest.raises(AssertionError): 40 | vehicle.add(_get_sample_lambda(), inputs=inputs) 41 | pytest.fail("inputs is not a list: %r" % inputs) 42 | 43 | 44 | def test_should_raise_assertion_on_non_list_outputs_for_add_part(): 45 | vehicle = dk.Vehicle() 46 | outputs = 'any' 47 | with pytest.raises(AssertionError): 48 | vehicle.add(_get_sample_lambda(), outputs=outputs) 49 | pytest.fail("outputs is not a list: %r" % outputs) 50 | 51 | 52 | def test_should_raise_assertion_on_non_boolean_threaded_for_add_part(): 53 | vehicle = dk.Vehicle() 54 | threaded = 'non_boolean' 55 | with pytest.raises(AssertionError): 56 | vehicle.add(_get_sample_lambda(), threaded=threaded) 57 | pytest.fail("threaded is not a boolean: %r" % threaded) -------------------------------------------------------------------------------- /donkeycar/parts/serial_controller.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Scrits to read signals from Arduino and convert into steering and throttle outputs 4 | Arduino input signal range: 0 to 200 5 | Output range: -1.00 to 1.00 6 | """ 7 | 8 | import serial 9 | import time 10 | 11 | class SerialController: 12 | def __init__(self): 13 | print("Starting Serial Controller") 14 | 15 | self.angle = 0.0 16 | self.throttle = 0.0 17 | self.mode = 'user' 18 | self.recording = False 19 | self.serial = serial.Serial('/dev/ttyS0', 115200, timeout=1) #Serial port - laptop: 'COM3', Arduino: '/dev/ttyACM0' 20 | 21 | 22 | def update(self): 23 | # delay on startup to avoid crashing 24 | print("Warming Serial Controller") 25 | time.sleep(3) 26 | 27 | while True: 28 | line = str(self.serial.readline().decode()).strip('\n').strip('\r') 29 | output = line.split(", ") 30 | if len(output) == 2: 31 | if output[0].isnumeric() and output[1].isnumeric(): 32 | self.angle = (float(output[0])-1500)/500 33 | self.throttle = (float(output[1])-1500)/500 34 | if self.throttle > 0.01: 35 | self.recording = True 36 | print("Recording") 37 | else: 38 | self.recording = False 39 | time.sleep(0.01) 40 | 41 | def run(self, img_arr=None): 42 | return self.run_threaded() 43 | 44 | def run_threaded(self, img_arr=None): 45 | #print("Signal:", self.angle, self.throttle) 46 | return self.angle, self.throttle, self.mode, self.recording 47 | -------------------------------------------------------------------------------- /donkeycar/memory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Sun Jun 25 11:07:48 2017 5 | 6 | @author: wroscoe 7 | """ 8 | 9 | class Memory: 10 | """ 11 | A convenience class to save key/value pairs. 12 | """ 13 | def __init__(self, *args, **kw): 14 | self.d = {} 15 | 16 | def __setitem__(self, key, value): 17 | if type(key) is str: 18 | self.d[key] = value 19 | else: 20 | if type(key) is not tuple: 21 | key = tuple(key) 22 | value = tuple(key) 23 | for i, k in enumerate(key): 24 | self.d[k] = value[i] 25 | 26 | def __getitem__(self, key): 27 | if type(key) is tuple: 28 | return [self.d[k] for k in key] 29 | else: 30 | return self.d[key] 31 | 32 | def update(self, new_d): 33 | self.d.update(new_d) 34 | 35 | def put(self, keys, inputs): 36 | if len(keys) > 1: 37 | for i, key in enumerate(keys): 38 | try: 39 | self.d[key] = inputs[i] 40 | except IndexError as e: 41 | error = str(e) + ' issue with keys: ' + str(key) 42 | raise IndexError(error) 43 | 44 | else: 45 | self.d[keys[0]] = inputs 46 | 47 | 48 | 49 | def get(self, keys): 50 | result = [self.d.get(k) for k in keys] 51 | return result 52 | 53 | def keys(self): 54 | return self.d.keys() 55 | 56 | def values(self): 57 | return self.d.values() 58 | 59 | def items(self): 60 | return self.d.items() 61 | -------------------------------------------------------------------------------- /donkeycar/parts/behavior.py: -------------------------------------------------------------------------------- 1 | class BehaviorPart(object): 2 | ''' 3 | Keep a list of states, and an active state. Keep track of switching. 4 | And return active state information. 5 | ''' 6 | def __init__(self, states): 7 | ''' 8 | expects a list of strings to enumerate state 9 | ''' 10 | print("bvh states:", states) 11 | self.states = states 12 | self.active_state = 0 13 | self.one_hot_state_array = [] 14 | for i in range(len(states)): 15 | self.one_hot_state_array.append(0.0) 16 | self.one_hot_state_array[0] = 1.0 17 | 18 | def increment_state(self): 19 | self.one_hot_state_array[self.active_state] = 0.0 20 | self.active_state += 1 21 | if self.active_state >= len(self.states): 22 | self.active_state = 0 23 | self.one_hot_state_array[self.active_state] = 1.0 24 | print("In State:", self.states[self.active_state]) 25 | 26 | def decrement_state(self): 27 | self.one_hot_state_array[self.active_state] = 0.0 28 | self.active_state -= 1 29 | if self.active_state < 0: 30 | self.active_state = len(self.states) - 1 31 | self.one_hot_state_array[self.active_state] = 1.0 32 | print("In State:", self.states[self.active_state]) 33 | 34 | def set_state(self, iState): 35 | self.one_hot_state_array[self.active_state] = 0.0 36 | self.active_state = iState 37 | self.one_hot_state_array[self.active_state] = 1.0 38 | print("In State:", self.states[self.active_state]) 39 | 40 | def run(self): 41 | return self.active_state, self.states[self.active_state], self.one_hot_state_array 42 | 43 | def shutdown(self): 44 | pass 45 | -------------------------------------------------------------------------------- /scripts/remote_cam_view.py: -------------------------------------------------------------------------------- 1 | """ 2 | Scripts to drive a donkey car remotely 3 | 4 | Usage: 5 | remote_cam_view.py --name= --broker="localhost" [--record=] 6 | 7 | 8 | Options: 9 | -h --help Show this screen. 10 | """ 11 | import os 12 | import time 13 | import math 14 | from docopt import docopt 15 | import donkeycar as dk 16 | import cv2 17 | 18 | from donkeycar.parts.cv import CvImageView, ImgBGR2RGB, ImgRGB2BGR, ImageScale, ImgWriter, ArrowKeyboardControls 19 | from donkeycar.parts.salient import SalientVis 20 | from donkeycar.parts.network import MQTTValuePub, MQTTValueSub 21 | from donkeycar.parts.transform import Lambda 22 | from donkeycar.parts.image import JpgToImgArr 23 | 24 | V = dk.vehicle.Vehicle() 25 | args = docopt(__doc__) 26 | print(args) 27 | 28 | V.add(MQTTValueSub(name="donkey/%s/camera" % args["--name"], broker=args["--broker"]), outputs=["jpg"]) 29 | V.add(JpgToImgArr(), inputs=["jpg"], outputs=["img_arr"]) 30 | V.add(ImgBGR2RGB(), inputs=["img_arr"], outputs=["rgb"]) 31 | V.add(ImageScale(4.0), inputs=["rgb"], outputs=["lg_img"]) 32 | V.add(CvImageView(), inputs=["lg_img"]) 33 | 34 | V.add(ArrowKeyboardControls(), outputs=["control"]) 35 | V.add(MQTTValuePub(name="donkey/%s/controls" % args["--name"]), inputs=["control"]) 36 | 37 | record_path = args["--record"] 38 | if record_path is not None: 39 | class ImageSaver: 40 | def __init__(self, path): 41 | self.index = 0 42 | self.path = path 43 | 44 | def run(self, img_arr): 45 | if img_arr is None: 46 | return 47 | dest_path = os.path.join(self.path, "img_%d.jpg" % self.index) 48 | self.index += 1 49 | cv2.imwrite(dest_path, img_arr) 50 | 51 | V.add(ImageSaver(record_path), inputs=["rgb"]) 52 | 53 | 54 | V.start(rate_hz=20) 55 | 56 | -------------------------------------------------------------------------------- /donkeycar/parts/web_controller/templates/wsTest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Websocket Test 7 | 8 | 9 | 10 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /donkeycar/parts/web_controller/templates/base_fpv.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Donkey FPV 6 | 7 | 62 | 63 | 64 |
65 | 66 |
67 | 68 | 69 | 70 |
71 |
72 | 73 |
74 | 75 | 76 | -------------------------------------------------------------------------------- /donkeycar/pipeline/augmentations.py: -------------------------------------------------------------------------------- 1 | import albumentations.core.transforms_interface 2 | import logging 3 | import albumentations as A 4 | from albumentations import GaussianBlur 5 | from albumentations.augmentations import RandomBrightnessContrast 6 | 7 | 8 | from donkeycar.config import Config 9 | 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class ImageAugmentation: 15 | def __init__(self, cfg, key, prob=0.5): 16 | aug_list = getattr(cfg, key, []) 17 | augmentations = [ImageAugmentation.create(a, cfg, prob) 18 | for a in aug_list] 19 | self.augmentations = A.Compose(augmentations) 20 | 21 | @classmethod 22 | def create(cls, aug_type: str, config: Config, prob) -> \ 23 | albumentations.core.transforms_interface.BasicTransform: 24 | """ Augmentation factory. Cropping and trapezoidal mask are 25 | transformations which should be applied in training, validation 26 | and inference. Multiply, Blur and similar are augmentations 27 | which should be used only in training. """ 28 | 29 | if aug_type == 'BRIGHTNESS': 30 | b_limit = getattr(config, 'AUG_BRIGHTNESS_RANGE', 0.2) 31 | logger.info(f'Creating augmentation {aug_type} {b_limit}') 32 | return RandomBrightnessContrast(brightness_limit=b_limit, 33 | contrast_limit=b_limit, 34 | p=prob) 35 | 36 | elif aug_type == 'BLUR': 37 | b_range = getattr(config, 'AUG_BLUR_RANGE', 3) 38 | logger.info(f'Creating augmentation {aug_type} {b_range}') 39 | return GaussianBlur(sigma_limit=b_range, blur_limit=(13, 13), 40 | p=prob) 41 | 42 | # Parts interface 43 | def run(self, img_arr): 44 | if len(self.augmentations) == 0: 45 | return img_arr 46 | aug_img_arr = self.augmentations(image=img_arr)["image"] 47 | return aug_img_arr 48 | 49 | -------------------------------------------------------------------------------- /scripts/freeze_model.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Usage: 3 | freeze_model.py --model="mymodel.h5" --output="frozen_model.pb" 4 | 5 | Note: 6 | This requires that TensorRT is setup correctly. For more instructions, take a look at 7 | https://docs.nvidia.com/deeplearning/sdk/tensorrt-install-guide/index.html 8 | ''' 9 | import os 10 | 11 | from docopt import docopt 12 | import json 13 | from pathlib import Path 14 | import tensorflow as tf 15 | 16 | args = docopt(__doc__) 17 | in_model = os.path.expanduser(args['--model']) 18 | output = os.path.expanduser(args['--output']) 19 | output_path = Path(output) 20 | output_meta = Path('%s/%s.metadata' % (output_path.parent.as_posix(), output_path.stem)) 21 | 22 | tf.compat.v1.disable_eager_execution() 23 | 24 | # Reset session 25 | tf.keras.backend.clear_session() 26 | tf.keras.backend.set_learning_phase(0) 27 | 28 | model = tf.compat.v1.keras.models.load_model(in_model, compile=False) 29 | session = tf.compat.v1.keras.backend.get_session() 30 | 31 | input_names = sorted([layer.op.name for layer in model.inputs]) 32 | output_names = sorted([layer.op.name for layer in model.outputs]) 33 | 34 | # Store additional information in metadata, useful for infrencing 35 | meta = {'input_names': input_names, 'output_names': output_names} 36 | 37 | graph = session.graph 38 | 39 | # Freeze Graph 40 | with graph.as_default(): 41 | # Convert variables to constants 42 | graph_frozen = tf.compat.v1.graph_util.convert_variables_to_constants(session, graph.as_graph_def(), output_names) 43 | # Remote training nodes 44 | graph_frozen = tf.compat.v1.graph_util.remove_training_nodes(graph_frozen) 45 | with open(output, 'wb') as output_file, open(output_meta.as_posix(), 'w') as meta_file: 46 | output_file.write(graph_frozen.SerializeToString()) 47 | meta_file.write(json.dumps(meta)) 48 | 49 | print ('Inputs = [%s], Outputs = [%s]' % (input_names, output_names)) 50 | print ('Writing metadata to %s' % output_meta.as_posix()) 51 | print ('To convert use: \n `convert-to-uff %s`' % (output)) 52 | 53 | -------------------------------------------------------------------------------- /scripts/multi_train.py: -------------------------------------------------------------------------------- 1 | ''' 2 | multi_train.py 3 | 4 | This script can be dropped into your car dir next to manage.py. 5 | This will invoke a number of sub processes to drive some ai clients 6 | and have them log into and drive on the SDSandbox donkey sim server. 7 | Check: https://docs.donkeycar.com/guide/simulator/ 8 | ''' 9 | import os 10 | import time 11 | import random 12 | import subprocess 13 | import uuid 14 | 15 | num_clients = 4 16 | model_file = "mtn_drv2.h5" #or any model in ~/mycar/models/ 17 | body_styles = ["donkey", "bare", "car01"] 18 | host = '127.0.0.1' 19 | procs = [] 20 | 21 | for i in range(num_clients): 22 | conf_file = "client%d.py" % i 23 | with open(conf_file, "wt") as outfile: 24 | outfile.write('WEB_CONTROL_PORT = 888%d\n' % i) 25 | outfile.write('WEB_INIT_MODE = "local"\n') 26 | outfile.write('DONKEY_GYM = True\n') 27 | outfile.write('DONKEY_GYM_ENV_NAME = "donkey-generated-track-v0"\n') 28 | outfile.write('DONKEY_SIM_PATH = "remote"\n') 29 | outfile.write('SIM_HOST = "%s"\n' % host) 30 | iStyle = random.randint(0, len(body_styles) - 1) 31 | body_style = body_styles[iStyle] 32 | r = random.randint(0, 255) 33 | g = random.randint(0, 255) 34 | b = random.randint(0, 255) 35 | outfile.write('GYM_CONF = { "body_style" : "%s", "body_rgb" : (%d, %d, %d), "car_name" : "ai%d", "font_size" : 100}\n' % (body_style, r, g, b, i+1)) 36 | outfile.write('GYM_CONF["racer_name"] = "ai-%d"\n' % (i+1)) 37 | outfile.write('GYM_CONF["country"] = "USA"\n') 38 | outfile.write('GYM_CONF["bio"] = "I am an ai!"\n') 39 | outfile.write('GYM_CONF["guid"] = "%d"\n' % i) 40 | outfile.close() 41 | 42 | command = "python manage.py drive --model=models/%s --myconfig=%s" % (model_file, conf_file) 43 | com_list = command.split(" ") 44 | print(com_list) 45 | proc = subprocess.Popen(com_list) 46 | procs.append(proc) 47 | time.sleep(2) 48 | 49 | 50 | print("running for 30 min...") 51 | try: 52 | time.sleep(60 * 30) 53 | except: 54 | pass 55 | 56 | print("stopping ai") 57 | for proc in procs: 58 | proc.kill() 59 | print('done') 60 | -------------------------------------------------------------------------------- /donkeycar/management/tub_web/static/style.css: -------------------------------------------------------------------------------- 1 | @media (min-width: 1600px) { 2 | .container { 3 | width: 1570px; 4 | } 5 | } 6 | 7 | .train-img { 8 | width: 320px; 9 | height: 240px; 10 | } 11 | 12 | #cur-frame { 13 | margin-bottom: 20px; 14 | padding: 5px; 15 | } 16 | 17 | img.clip-thumbnail { 18 | width: 6.25%; 19 | } 20 | 21 | tr.active td:first-child, 22 | tr.active td:last-child { 23 | background-color: #5bc0de !important; 24 | } 25 | 26 | tbody#clips td { 27 | vertical-align: middle; 28 | } 29 | .affix { 30 | top: 0; 31 | width: 100%; 32 | } 33 | 34 | /* https://stackoverflow.com/questions/20652581/bootstrap-affix-nav-causes-div-below-to-jump-up */ 35 | .affix-wrapper { 36 | min-height: 272px; 37 | } 38 | 39 | .affix + #clips-container { 40 | padding-top: 272; 41 | } 42 | 43 | .preview { 44 | width: 100%; 45 | } 46 | 47 | .preview-centered { 48 | width: 460px; 49 | margin: 0 auto; 50 | padding-bottom: 12px; 51 | } 52 | 53 | .preview-wrapper { 54 | display: flex; 55 | } 56 | 57 | .preview-img { 58 | flex: 0 0 65%; 59 | } 60 | 61 | .preview-toolbar { 62 | flex: 1; 63 | background-color: white; 64 | } 65 | 66 | .preview-toolbar button { 67 | margin: 5px; 68 | width: 140px; 69 | } 70 | 71 | .preview-toolbar select { 72 | margin: 5px; 73 | width: 140px; 74 | } 75 | 76 | tbody#clips .progress { 77 | height: 3px; 78 | margin-bottom: 10px; 79 | } 80 | 81 | .progress .progress-bar { 82 | -webkit-transition: none; 83 | -moz-transition: none; 84 | -ms-transition: none; 85 | -o-transition: none; 86 | transition: none; 87 | } 88 | 89 | .steering-bar .progress { 90 | width: 48%; 91 | margin-bottom: 0px; 92 | border-radius: 0px; 93 | float: left; 94 | } 95 | 96 | .steering-bar .progress.negative { 97 | border-right-style: solid; 98 | border-right-width: thin; 99 | border-right-color: red; 100 | } 101 | 102 | .steering-bar .progress.positive { 103 | border-left-style: solid; 104 | border-left-width: thin; 105 | border-left-color: red; 106 | } 107 | -------------------------------------------------------------------------------- /donkeycar/tests/test_memory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import unittest 4 | import pytest 5 | from donkeycar.memory import Memory 6 | 7 | class TestMemory(unittest.TestCase): 8 | 9 | def test_setitem_single_item(self): 10 | mem = Memory() 11 | mem['myitem'] = 999 12 | assert mem['myitem'] == 999 13 | 14 | def test_setitem_multi_items(self): 15 | mem = Memory() 16 | mem[('myitem1', 'myitem2')] = [888, '999'] 17 | assert mem[('myitem1', 'myitem2')] == [888, '999'] 18 | 19 | def test_put_single_item(self): 20 | mem = Memory() 21 | mem.put(['myitem'], 999) 22 | assert mem['myitem'] == 999 23 | 24 | def test_put_single_item_as_tuple(self): 25 | mem = Memory() 26 | mem.put(('myitem',), 999) 27 | assert mem['myitem'] == 999 28 | 29 | def test_put_multi_item(self): 30 | mem = Memory() 31 | mem.put(['my1stitem','my2nditem'], [777, '999']) 32 | assert mem['my1stitem'] == 777 33 | assert mem['my2nditem'] == '999' 34 | 35 | def test_put_multi_item_as_tuple(self): 36 | mem = Memory() 37 | mem.put(('my1stitem','my2nditem'), (777, '999')) 38 | assert mem['my1stitem'] == 777 39 | assert mem['my2nditem'] == '999' 40 | 41 | def test_get_multi_item(self): 42 | mem = Memory() 43 | mem.put(['my1stitem','my2nditem'], [777, '999']) 44 | assert mem.get(['my1stitem','my2nditem']) == [777, '999'] 45 | 46 | def test_update_item(self): 47 | mem = Memory() 48 | mem.put(['myitem'], 888) 49 | assert mem['myitem'] == 888 50 | 51 | mem.update({'myitem': '111'}) 52 | assert mem['myitem'] == '111' 53 | 54 | def test_get_keys(self): 55 | mem = Memory() 56 | mem.put(['myitem'], 888) 57 | assert list(mem.keys()) == ['myitem'] 58 | 59 | def test_get_values(self): 60 | mem = Memory() 61 | mem.put(['myitem'], 888) 62 | assert list(mem.values()) == [888] 63 | 64 | def test_get_iter(self): 65 | mem = Memory() 66 | mem.put(['myitem'], 888) 67 | 68 | assert dict(mem.items()) == {'myitem': 888} 69 | -------------------------------------------------------------------------------- /donkeycar/templates/cfg_arduino_drive.py: -------------------------------------------------------------------------------- 1 | """ 2 | CAR CONFIG 3 | 4 | This file is read by your car application's manage.py script to change the car 5 | performance. 6 | 7 | EXMAPLE 8 | ----------- 9 | import dk 10 | cfg = dk.load_config(config_path='~/mycar/config.py') 11 | print(cfg.CAMERA_RESOLUTION) 12 | 13 | """ 14 | 15 | 16 | import os 17 | 18 | #PATHS 19 | CAR_PATH = PACKAGE_PATH = os.path.dirname(os.path.realpath(__file__)) 20 | DATA_PATH = os.path.join(CAR_PATH, 'data') 21 | MODELS_PATH = os.path.join(CAR_PATH, 'models') 22 | 23 | #VEHICLE 24 | DRIVE_LOOP_HZ = 20 25 | MAX_LOOPS = None 26 | 27 | #STEERING 28 | STEERING_ARDUINO_PIN = 6 29 | STEERING_ARDUINO_LEFT_PWM = 120 30 | STEERING_ARDUINO_RIGHT_PWM = 40 31 | 32 | #THROTTLE 33 | THROTTLE_ARDUINO_PIN = 5 34 | THROTTLE_ARDUINO_FORWARD_PWM = 105 35 | THROTTLE_ARDUINO_STOPPED_PWM = 90 36 | THROTTLE_ARDUINO_REVERSE_PWM = 75 37 | 38 | #JOYSTICK 39 | USE_JOYSTICK_AS_DEFAULT = False #when starting the manage.py, when True, will not require a --js option to use the joystick 40 | JOYSTICK_MAX_THROTTLE = 0.8 #this scalar is multiplied with the -1 to 1 throttle value to limit the maximum throttle. This can help if you drop the controller or just don't need the full speed available. 41 | JOYSTICK_STEERING_SCALE = 1.0 #some people want a steering that is less sensitve. This scalar is multiplied with the steering -1 to 1. It can be negative to reverse dir. 42 | AUTO_RECORD_ON_THROTTLE = True #if true, we will record whenever throttle is not zero. if false, you must manually toggle recording with some other trigger. Usually circle button on joystick. 43 | CONTROLLER_TYPE='F710' #(ps3|ps4|xbox|nimbus|wiiu|F710|rc3) 44 | USE_NETWORKED_JS = False #should we listen for remote joystick control over the network? 45 | NETWORK_JS_SERVER_IP = "192.168.0.1"#when listening for network joystick control, which ip is serving this information 46 | JOYSTICK_DEADZONE = 0.0 # when non zero, this is the smallest throttle before recording triggered. 47 | JOYSTICK_THROTTLE_DIR = -1.0 # use -1.0 to flip forward/backward, use 1.0 to use joystick's natural forward/backward 48 | USE_FPV = False # send camera data to FPV webserver 49 | -------------------------------------------------------------------------------- /donkeycar/parts/simulation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Parts to try donkeycar without a physical car. 5 | """ 6 | 7 | import random 8 | import numpy as np 9 | 10 | 11 | class MovingSquareTelemetry: 12 | """ 13 | Generator of cordinates of a bouncing moving square for simulations. 14 | """ 15 | def __init__(self, max_velocity=29, 16 | x_min=10, x_max=150, 17 | y_min=10, y_max=110): 18 | 19 | self.velocity = random.random() * max_velocity 20 | 21 | self.x_min, self.x_max = x_min, x_max 22 | self.y_min, self.y_max = y_min, y_max 23 | 24 | self.x_direction = random.random() * 2 - 1 25 | self.y_direction = random.random() * 2 - 1 26 | 27 | self.x = random.random() * x_max 28 | self.y = random.random() * y_max 29 | 30 | self.tel = self.x, self.y 31 | 32 | def run(self): 33 | # move 34 | self.x += self.x_direction * self.velocity 35 | self.y += self.y_direction * self.velocity 36 | 37 | # make square bounce off walls 38 | if self.y < self.y_min or self.y > self.y_max: 39 | self.y_direction *= -1 40 | if self.x < self.x_min or self.x > self.x_max: 41 | self.x_direction *= -1 42 | 43 | return int(self.x), int(self.y) 44 | 45 | def update(self): 46 | self.tel = self.run() 47 | 48 | def run_threaded(self): 49 | return self.tel 50 | 51 | 52 | class SquareBoxCamera: 53 | """ 54 | Fake camera that returns an image with a square box. 55 | 56 | This can be used to test if a learning algorithm can learn. 57 | """ 58 | 59 | def __init__(self, resolution=(120, 160), box_size=4, color=(255, 0, 0)): 60 | self.resolution = resolution 61 | self.box_size = box_size 62 | self.color = color 63 | 64 | def run(self, x, y, box_size=None, color=None): 65 | """ 66 | Create an image of a square box at a given coordinates. 67 | """ 68 | radius = int((box_size or self.box_size)/2) 69 | color = color or self.color 70 | frame = np.zeros(shape=self.resolution + (3,)) 71 | frame[y - radius: y + radius, 72 | x - radius: x + radius, :] = color 73 | return frame -------------------------------------------------------------------------------- /donkeycar/tests/test_launch.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import time 3 | 4 | from donkeycar import utils 5 | from donkeycar.parts.launch import * 6 | 7 | 8 | def test_ai_launch(): 9 | ai_launch = AiLaunch(launch_duration=1.0, launch_throttle=1.0) 10 | 11 | mode = "user" 12 | ai_throttle = 0.0 13 | 14 | new_throttle = ai_launch.run(mode, ai_throttle) 15 | 16 | assert(new_throttle == 0.0) 17 | 18 | mode = "local" 19 | 20 | new_throttle = ai_launch.run(mode, ai_throttle) 21 | 22 | assert(new_throttle == 0.0) 23 | 24 | mode = "user" 25 | 26 | new_throttle = ai_launch.run(mode, ai_throttle) 27 | 28 | assert(new_throttle == 0.0) 29 | 30 | ai_launch.enable_ai_launch() 31 | mode = "local" 32 | 33 | new_throttle = ai_launch.run(mode, ai_throttle) 34 | 35 | assert(new_throttle == 1.0) 36 | 37 | time.sleep(1.1) 38 | 39 | new_throttle = ai_launch.run(mode, ai_throttle) 40 | 41 | assert(new_throttle == 0.0) 42 | 43 | 44 | def test_ai_launch_keep_enabled(): 45 | ai_launch = AiLaunch(launch_duration=1.0, launch_throttle=1.0, keep_enabled=True) 46 | 47 | mode = "user" 48 | ai_throttle = 0.0 49 | 50 | new_throttle = ai_launch.run(mode, ai_throttle) 51 | 52 | assert(new_throttle == 0.0) 53 | 54 | mode = "local" 55 | 56 | new_throttle = ai_launch.run(mode, ai_throttle) 57 | 58 | assert(new_throttle == 1.0) 59 | 60 | new_throttle = ai_launch.run(mode, ai_throttle) 61 | 62 | time.sleep(1.1) 63 | 64 | new_throttle = ai_launch.run(mode, ai_throttle) 65 | assert(new_throttle == 0.0) 66 | 67 | mode = "user" 68 | 69 | new_throttle = ai_launch.run(mode, ai_throttle) 70 | new_throttle = ai_launch.run(mode, ai_throttle) 71 | 72 | assert(ai_launch.enabled==False) 73 | assert(new_throttle == 0.0) 74 | 75 | mode = "local" 76 | 77 | new_throttle = ai_launch.run(mode, ai_throttle) 78 | 79 | assert(new_throttle == 1.0) 80 | 81 | time.sleep(1.1) 82 | 83 | new_throttle = ai_launch.run(mode, ai_throttle) 84 | 85 | mode = "user" 86 | 87 | new_throttle = ai_launch.run(mode, ai_throttle) 88 | new_throttle = ai_launch.run(mode, ai_throttle) 89 | 90 | assert(new_throttle == 0.0) 91 | 92 | -------------------------------------------------------------------------------- /scripts/pigpio_donkey.py: -------------------------------------------------------------------------------- 1 | ''' 2 | # pigpio_donkey.py 3 | # author: Tawn Kramer 4 | # date: 3/11/2018 5 | # 6 | # Use the pigpio python module and daemon to get hardware pwm controls from 7 | # a raspberrypi gpio pins and no additional hardware. 8 | # 9 | # Install and setup: 10 | # sudo update && sudo apt install pigpio python3-pigpio& sudo systemctl start pigpiod 11 | ''' 12 | import os 13 | import donkeycar as dk 14 | from donkeycar.parts.controller import PS3JoystickController 15 | from donkeycar.parts.actuator import PWMSteering, PWMThrottle 16 | 17 | import pigpio 18 | 19 | class PiGPIO_PWM(): 20 | def __init__(self, pin, pgio, freq=75): 21 | self.pin = pin 22 | self.pgio = pgio 23 | self.freq = freq 24 | self.pgio.set_mode(self.pin, pigpio.OUTPUT) 25 | 26 | def __del__(self): 27 | self.pgio.stop() 28 | 29 | def set_pulse(self, pulse): 30 | self.pgio.hardware_PWM(self.pin, self.freq, pulse) 31 | 32 | def run(self, pulse): 33 | self.set_pulse(pulse) 34 | 35 | 36 | cfg = dk.load_config() 37 | 38 | p = pigpio.pi() 39 | 40 | V = dk.Vehicle() 41 | 42 | cfg.STEERING_CHANNEL = 12 43 | cfg.THROTTLE_CHANNEL = 13 44 | 45 | PULSE_MULT = 1000 46 | 47 | cfg.STEERING_LEFT_PWM = 40 * PULSE_MULT 48 | cfg.STEERING_RIGHT_PWM = 170 * PULSE_MULT 49 | 50 | cfg.THROTTLE_FORWARD_PWM = 170 * PULSE_MULT 51 | cfg.THROTTLE_STOPPED_PWM = 105 * PULSE_MULT 52 | cfg.THROTTLE_REVERSE_PWM = 40 * PULSE_MULT 53 | 54 | V.add(PS3JoystickController(), inputs=['camera/arr'], 55 | outputs=['angle', 'throttle', 'mode', 'recording'], 56 | threaded=True) 57 | 58 | steering_controller = PiGPIO_PWM(cfg.STEERING_CHANNEL, p) 59 | steering = PWMSteering(controller=steering_controller, 60 | left_pulse=cfg.STEERING_LEFT_PWM, 61 | right_pulse=cfg.STEERING_RIGHT_PWM) 62 | 63 | throttle_controller = PiGPIO_PWM(cfg.THROTTLE_CHANNEL, p) 64 | throttle = PWMThrottle(controller=throttle_controller, 65 | max_pulse=cfg.THROTTLE_FORWARD_PWM, 66 | zero_pulse=cfg.THROTTLE_STOPPED_PWM, 67 | min_pulse=cfg.THROTTLE_REVERSE_PWM) 68 | 69 | V.add(steering, inputs=['angle']) 70 | V.add(throttle, inputs=['throttle']) 71 | 72 | V.start() 73 | 74 | 75 | -------------------------------------------------------------------------------- /donkeycar/parts/pytorch/torch_train.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | import torch 4 | import pytorch_lightning as pl 5 | from pytorch_lightning.utilities.model_summary import summarize 6 | from donkeycar.parts.pytorch.torch_data import TorchTubDataModule 7 | from donkeycar.parts.pytorch.torch_utils import get_model_by_type 8 | 9 | 10 | def train(cfg, tub_paths, model_output_path, model_type, checkpoint_path=None): 11 | """ 12 | Train the model 13 | """ 14 | model_name, model_ext = os.path.splitext(model_output_path) 15 | 16 | is_torch_model = model_ext == '.ckpt' 17 | if is_torch_model: 18 | model = f'{model_name}.ckpt' 19 | else: 20 | print(f"Unrecognized model file extension for model_output_path: '" 21 | f"{model_output_path}'. Please use the '.ckpt' extension.") 22 | 23 | if not model_type: 24 | model_type = cfg.DEFAULT_MODEL_TYPE 25 | 26 | tubs = tub_paths.split(',') 27 | tub_paths = [os.path.expanduser(tub) for tub in tubs] 28 | output_path = os.path.expanduser(model_output_path) 29 | output_dir = str(Path(model_output_path).parent) 30 | model = get_model_by_type(model_type, cfg, checkpoint_path=checkpoint_path) 31 | if torch.cuda.is_available(): 32 | print('Using CUDA') 33 | gpus = -1 34 | else: 35 | print('Not using CUDA') 36 | gpus = 0 37 | 38 | logger = None 39 | if cfg.VERBOSE_TRAIN: 40 | print("Tensorboard logging started. Run `tensorboard --logdir " 41 | "./tb_logs` in a new terminal") 42 | from pytorch_lightning.loggers import TensorBoardLogger 43 | 44 | # Create Tensorboard logger 45 | logger = TensorBoardLogger('tb_logs', name=model_name) 46 | 47 | if cfg.PRINT_MODEL_SUMMARY: 48 | summarize(model) 49 | trainer = pl.Trainer(accelerator='cpu', logger=logger, 50 | max_epochs=cfg.MAX_EPOCHS, default_root_dir=output_dir) 51 | 52 | data_module = TorchTubDataModule(cfg, tub_paths) 53 | trainer.fit(model, data_module) 54 | 55 | if is_torch_model: 56 | checkpoint_model_path = f'{os.path.splitext(output_path)[0]}.ckpt' 57 | trainer.save_checkpoint(checkpoint_model_path) 58 | print("Saved final model to {}".format(checkpoint_model_path)) 59 | 60 | return model.loss_history 61 | -------------------------------------------------------------------------------- /donkeycar/management/ui/ui.kv: -------------------------------------------------------------------------------- 1 | 2 | : 3 | BoxLayout: 4 | padding: 10 5 | orientation: 'vertical' 6 | Image: 7 | source: root.img_path 8 | size: self.texture_size 9 | 10 | 11 | BoxLayout: 12 | orientation: 'vertical' 13 | ActionView: 14 | id: av 15 | size_hint_y: None 16 | height: reduced_height 17 | 18 | ActionPrevious: 19 | with_previous: False 20 | app_icon: '' 21 | size_hint_x: None 22 | width: 0 23 | ActionButton: 24 | id: tub_btn 25 | color: font_color 26 | text: 'Tub Manager' 27 | on_release: 28 | sm.current = 'tub' 29 | sm.current_screen.bind_keyboard() 30 | ActionButton: 31 | id: train_btn 32 | color: font_color 33 | text: 'Trainer' 34 | on_release: 35 | sm.current = 'train' 36 | sm.current_screen.bind_keyboard() 37 | ActionButton: 38 | id: pilot_btn 39 | color: font_color 40 | text: 'Pilot Arena' 41 | on_release: 42 | sm.current = 'pilot' 43 | if not sm.current_screen.index: sm.current_screen.index = 0 44 | sm.current_screen.bind_keyboard() 45 | ActionButton: 46 | id: car_btn 47 | color: font_color 48 | text: 'Car Connector' 49 | on_release: 50 | sm.current = 'car' 51 | sm.current_screen.update_pilots() 52 | sm.current_screen.bind_keyboard() 53 | ActionButton: 54 | color: font_color 55 | text: 'Quit' 56 | on_release: app.get_running_app().stop() 57 | 58 | ScreenManager: 59 | id: sm 60 | StartScreen: 61 | id: start_screen 62 | name: 'start' 63 | TubScreen: 64 | id: tub_screen 65 | name: 'tub' 66 | TrainScreen: 67 | id: train_screen 68 | name: 'train' 69 | PilotScreen: 70 | id: pilot_screen 71 | name: 'pilot' 72 | CarScreen: 73 | id: car_screen 74 | name: 'car' 75 | 76 | StatusBar: 77 | id: status 78 | text: "Donkey ready" -------------------------------------------------------------------------------- /donkeycar/parts/leopard_imaging.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | from donkeycar.parts.camera import BaseCamera 3 | from donkeycar.parts.fast_stretch import fast_stretch 4 | import time 5 | 6 | 7 | class LICamera(BaseCamera): 8 | ''' 9 | The Leopard Imaging Camera with Fast-Stretch built in. 10 | ''' 11 | def __init__(self, width=224, height=224, capture_width=1280, capture_height=720, fps=60): 12 | super(LICamera, self).__init__() 13 | self.width = width 14 | self.height = height 15 | self.capture_width = capture_width 16 | self.capture_height = capture_height 17 | self.fps = fps 18 | self.camera_id = LICamera.camera_id(self.capture_width, self.capture_height, self.width, self.height, self.fps) 19 | self.frame = None 20 | print('Connecting to Leopard Imaging Camera') 21 | self.capture = cv2.VideoCapture(self.camera_id) 22 | time.sleep(2) 23 | if self.capture.isOpened(): 24 | print('Leopard Imaging Camera Connected.') 25 | self.on = True 26 | else: 27 | self.on = False 28 | print('Unable to connect. Are you sure you are using the right camera parameters ?') 29 | 30 | def read_frame(self): 31 | success, frame = self.capture.read() 32 | if success: 33 | # returns an RGB frame. 34 | frame = fast_stretch(frame) 35 | self.frame = frame 36 | 37 | def run(self): 38 | self.read_frame() 39 | return self.frame 40 | 41 | def update(self): 42 | # keep looping infinitely until the thread is stopped 43 | # if the thread indicator variable is set, stop the thread 44 | while self.on: 45 | self.read_frame() 46 | 47 | def shutdown(self): 48 | # indicate that the thread should be stopped 49 | self.on = False 50 | print('Stopping Leopard Imaging Camera') 51 | self.capture.release() 52 | time.sleep(.5) 53 | 54 | @classmethod 55 | def camera_id(cls, capture_width, capture_height, width, height, fps): 56 | return 'nvarguscamerasrc ! video/x-raw(memory:NVMM), width=%d, height=%d, format=(string)NV12, framerate=(fraction)%d/1 ! nvvidconv ! video/x-raw, width=(int)%d, height=(int)%d, format=(string)BGRx ! videoconvert ! appsink' % ( 57 | capture_width, capture_height, fps, width, height) -------------------------------------------------------------------------------- /donkeycar/management/ui/ui.py: -------------------------------------------------------------------------------- 1 | import os 2 | # need to do this before importing anything else 3 | os.environ['KIVY_LOG_MODE'] = 'MIXED' 4 | 5 | from kivy.logger import Logger, LOG_LEVELS 6 | from kivy.clock import Clock 7 | from kivy.app import App 8 | from kivy.properties import StringProperty 9 | from kivy.uix.boxlayout import BoxLayout 10 | from kivy.lang.builder import Builder 11 | from kivy.core.window import Window 12 | 13 | from donkeycar.management.ui.car_screen import CarScreen 14 | from donkeycar.management.ui.pilot_screen import PilotScreen 15 | from donkeycar.management.ui.rc_file_handler import rc_handler 16 | from donkeycar.management.ui.train_screen import TrainScreen 17 | from donkeycar.management.ui.tub_screen import TubScreen 18 | from donkeycar.management.ui.common import AppScreen 19 | 20 | Logger.setLevel(LOG_LEVELS["info"]) 21 | Window.size = (800, 800) 22 | 23 | 24 | class Header(BoxLayout): 25 | title = StringProperty() 26 | description = StringProperty() 27 | 28 | 29 | class StartScreen(AppScreen): 30 | img_path = os.path.realpath(os.path.join( 31 | os.path.dirname(__file__), 32 | '../../parts/web_controller/templates/' 33 | 'static/donkeycar-logo-sideways.png')) 34 | pass 35 | 36 | 37 | class DonkeyApp(App): 38 | title = 'Donkey Car' 39 | 40 | def initialise(self, event): 41 | self.root.ids.tub_screen.ids.config_manager.load_action() 42 | self.root.ids.train_screen.initialise() 43 | self.root.ids.pilot_screen.initialise(event) 44 | self.root.ids.car_screen.initialise() 45 | self.root.ids.tub_screen.ids.tub_loader.update_tub() 46 | self.root.ids.status.text = 'Donkey ready' 47 | 48 | def build(self): 49 | # the builder returns the screen manager in ui.kv file 50 | for kv in ['common.kv', 'tub_screen.kv', 'train_screen.kv', 51 | 'pilot_screen.kv', 'car_screen.kv', 'ui.kv']: 52 | dm = Builder.load_file(os.path.join(os.path.dirname(__file__), kv)) 53 | Clock.schedule_once(self.initialise) 54 | return dm 55 | 56 | def on_stop(self, *args): 57 | tub = self.root.ids.tub_screen.ids.tub_loader.tub 58 | if tub: 59 | tub.close() 60 | Logger.info("App: Good bye Donkey") 61 | 62 | 63 | def main(): 64 | DonkeyApp().run() 65 | 66 | 67 | if __name__ == '__main__': 68 | main() 69 | -------------------------------------------------------------------------------- /donkeycar/tests/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import pytest 4 | from donkeycar.parts.tub_v2 import Tub 5 | from donkeycar.parts.simulation import SquareBoxCamera, MovingSquareTelemetry 6 | from donkeycar.management.base import CreateCar 7 | 8 | 9 | def on_pi(): 10 | if 'aarch64' in platform.machine(): 11 | return True 12 | return False 13 | 14 | 15 | temp_tub_path = None 16 | 17 | 18 | @pytest.fixture 19 | def tub_path(tmpdir): 20 | tub_path = tmpdir.mkdir('tubs').join('tub') 21 | return str(tub_path) 22 | 23 | 24 | @pytest.fixture 25 | def tub(tub_path): 26 | t = create_sample_tub(tub_path, records=128) 27 | return t 28 | 29 | 30 | @pytest.fixture 31 | def tubs(tmpdir, tubs=5): 32 | tubs_dir = tmpdir.mkdir('tubs') 33 | tub_paths = [str(tubs_dir.join('tub_{}'.format(i))) for i in range(tubs)] 34 | tubs = [create_sample_tub(tub_path, records=5) for tub_path in tub_paths] 35 | return str(tubs_dir), tub_paths, tubs 36 | 37 | 38 | def create_sample_tub(path, records=128): 39 | inputs = ['cam/image_array', 'user/angle', 'user/throttle', 40 | 'location/one_hot_state_array'] 41 | types = ['image_array', 'float', 'float', 'vector'] 42 | t = Tub(path, inputs=inputs, types=types) 43 | cam = SquareBoxCamera() 44 | tel = MovingSquareTelemetry() 45 | num_loc = 10 46 | for _ in range(records): 47 | x, y = tel.run() 48 | img_arr = cam.run(x, y) 49 | loc = [0.0] * num_loc 50 | loc[1] = 1.0 51 | t.write_record( 52 | {'cam/image_array': img_arr, 'user/angle': x, 'user/throttle': y, 53 | 'location/one_hot_state_array': loc}) 54 | 55 | global temp_tub_path 56 | temp_tub_path = t 57 | print("setting temp tub path to:", temp_tub_path) 58 | 59 | return t 60 | 61 | 62 | def d2_path(temp_path): 63 | path = os.path.join(temp_path, 'd2') 64 | return str(path) 65 | 66 | 67 | def default_template(car_dir): 68 | c = CreateCar() 69 | c.create_car(car_dir, template='complete', overwrite=True) 70 | return car_dir 71 | 72 | 73 | def custom_template(car_dir, template): 74 | c = CreateCar() 75 | c.create_car(car_dir, template=template, overwrite=True) 76 | return car_dir 77 | 78 | 79 | def create_sample_record(): 80 | cam = SquareBoxCamera() 81 | tel = MovingSquareTelemetry() 82 | x, y = tel.run() 83 | img_arr = cam.run(x, y) 84 | return {'cam/image_array': img_arr, 'angle': x, 'throttle': y} 85 | -------------------------------------------------------------------------------- /donkeycar/templates/arduino_drive.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Scripts to drive a donkey 2 car 4 | Shows how to use an implement the drive-loop for a car with Arduino as its 5 | drive train. Further it shows how to control the car with a joystick for the 6 | sake of providing a functional demo. 7 | 8 | Usage: 9 | manage.py (drive) 10 | 11 | Options: 12 | -h --help Show this screen. 13 | """ 14 | import os 15 | import time 16 | 17 | from docopt import docopt 18 | 19 | import donkeycar as dk 20 | from donkeycar.parts.actuator import ArduinoFirmata, ArdPWMSteering, ArdPWMThrottle 21 | from donkeycar.parts.controller import get_js_controller 22 | 23 | 24 | def drive(cfg): 25 | ''' 26 | Construct a working robotic vehicle from many parts. 27 | Each part runs as a job in the Vehicle loop, calling either 28 | it's run or run_threaded method depending on the constructor flag `threaded`. 29 | All parts are updated one after another at the framerate given in 30 | cfg.DRIVE_LOOP_HZ assuming each part finishes processing in a timely manner. 31 | Parts may have named outputs and inputs. The framework handles passing named outputs 32 | to parts requesting the same named input. 33 | ''' 34 | 35 | #Initialize car 36 | V = dk.vehicle.Vehicle() 37 | ctr = get_js_controller(cfg) 38 | V.add(ctr, 39 | outputs=['user/angle', 'user/throttle', 'user/mode', 'recording'], 40 | threaded=True) 41 | 42 | #Drive train setup 43 | arduino_controller = ArduinoFirmata( 44 | servo_pin=cfg.STEERING_ARDUINO_PIN, esc_pin=cfg.THROTTLE_ARDUINO_PIN) 45 | steering = ArdPWMSteering(controller=arduino_controller, 46 | left_pulse=cfg.STEERING_ARDUINO_LEFT_PWM, 47 | right_pulse=cfg.STEERING_ARDUINO_RIGHT_PWM) 48 | 49 | throttle = ArdPWMThrottle(controller=arduino_controller, 50 | max_pulse=cfg.THROTTLE_ARDUINO_FORWARD_PWM, 51 | zero_pulse=cfg.THROTTLE_ARDUINO_STOPPED_PWM, 52 | min_pulse=cfg.THROTTLE_ARDUINO_REVERSE_PWM) 53 | 54 | V.add(steering, inputs=['user/angle']) 55 | V.add(throttle, inputs=['user/throttle']) 56 | 57 | #run the vehicle 58 | V.start(rate_hz=cfg.DRIVE_LOOP_HZ, 59 | max_loop_count=cfg.MAX_LOOPS) 60 | 61 | 62 | if __name__ == '__main__': 63 | args = docopt(__doc__) 64 | cfg = dk.load_config() 65 | 66 | if args['drive']: 67 | drive(cfg) 68 | -------------------------------------------------------------------------------- /donkeycar/parts/tfmini.py: -------------------------------------------------------------------------------- 1 | import time 2 | import serial 3 | import logging 4 | 5 | ''' 6 | Note about poll delay: 7 | 8 | Without poll delay the part only outputs a few values before failing. 9 | This is probably because the serial port's input buffer is not clearing fast enough to run 10 | without a poll delay. Calling self.ser.reset_input_buffer() (line 58) helps, but 11 | does not completely solve the issue. Keep in mind at a car speed of 4 m/s this introduces 12 | a systematic error of 4cm, using a poll delay of 10 ms. 13 | ''' 14 | 15 | class TFMini: 16 | """ 17 | Class for TFMini and TFMini-Plus distance sensors. 18 | See wiring and installation instructions at https://github.com/TFmini/TFmini-RaspberryPi 19 | 20 | Returns distance in centimeters. 21 | """ 22 | 23 | def __init__(self, port="/dev/serial0", baudrate=115200, poll_delay=0.01, init_delay=0.1): 24 | self.ser = serial.Serial(port, baudrate) 25 | self.poll_delay = poll_delay 26 | 27 | self.dist = 0 28 | 29 | if not self.ser.is_open: 30 | self.ser.close() # in case it is still open, we do not want to open it twice 31 | self.ser.open() 32 | 33 | self.logger = logging.getLogger(__name__) 34 | 35 | self.logger.info("Init TFMini") 36 | time.sleep(init_delay) 37 | 38 | def update(self): 39 | while self.ser.is_open: 40 | self.poll() 41 | if self.poll_delay > 0: 42 | time.sleep(self.poll_delay) 43 | 44 | def poll(self): 45 | try: 46 | count = self.ser.in_waiting 47 | if count > 8: 48 | recv = self.ser.read(9) 49 | self.ser.reset_input_buffer() 50 | 51 | if recv[0] == 0x59 and recv[1] == 0x59: 52 | dist = recv[2] + recv[3] * 256 53 | strength = recv[4] + recv[5] * 256 54 | 55 | if strength > 0: 56 | self.dist = dist 57 | 58 | self.ser.reset_input_buffer() 59 | 60 | except Exception as e: 61 | self.logger.error(e) 62 | 63 | 64 | def run_threaded(self): 65 | return self.dist 66 | 67 | def run(self): 68 | self.poll() 69 | return self.dist 70 | 71 | def shutdown(self): 72 | self.ser.close() 73 | 74 | if __name__ == "__main__": 75 | lidar = TFMini(poll_delay=0.1) 76 | 77 | for i in range(100): 78 | data = lidar.run() 79 | print(i, data) 80 | 81 | lidar.shutdown() 82 | -------------------------------------------------------------------------------- /donkeycar/templates/cfg_square.py: -------------------------------------------------------------------------------- 1 | """ 2 | CAR CONFIG 3 | 4 | This file is read by your car application's manage.py script to change the car 5 | performance. 6 | 7 | EXMAPLE 8 | ----------- 9 | import dk 10 | cfg = dk.load_config(config_path='~/mycar/config.py') 11 | print(cfg.CAMERA_RESOLUTION) 12 | 13 | """ 14 | 15 | 16 | import os 17 | 18 | #PATHS 19 | CAR_PATH = PACKAGE_PATH = os.path.dirname(os.path.realpath(__file__)) 20 | DATA_PATH = os.path.join(CAR_PATH, 'data') 21 | MODELS_PATH = os.path.join(CAR_PATH, 'models') 22 | 23 | #VEHICLE 24 | DRIVE_LOOP_HZ = 20 25 | MAX_LOOPS = 100000 26 | 27 | #CAMERA 28 | IMAGE_W = 160 29 | IMAGE_H = 120 30 | IMAGE_DEPTH = 3 # default RGB=3, make 1 for mono 31 | 32 | #9865, over rides only if needed, ie. TX2.. 33 | PCA9685_I2C_ADDR = 0x40 34 | PCA9685_I2C_BUSNUM = None 35 | 36 | #STEERING 37 | STEERING_CHANNEL = 1 38 | STEERING_LEFT_PWM = 460 39 | STEERING_RIGHT_PWM = 290 40 | 41 | #THROTTLE 42 | THROTTLE_CHANNEL = 0 43 | THROTTLE_FORWARD_PWM = 500 44 | THROTTLE_STOPPED_PWM = 370 45 | THROTTLE_REVERSE_PWM = 220 46 | 47 | #TRAINING 48 | DEFAULT_MODEL_TYPE = 'linear' #(linear|categorical|rnn|imu|behavior|3d|localizer|latent) 49 | BATCH_SIZE = 128 50 | TRAIN_TEST_SPLIT = 0.8 51 | MAX_EPOCHS = 100 52 | SHOW_PLOT = True 53 | VERBOSE_TRAIN = True 54 | USE_EARLY_STOP = True 55 | EARLY_STOP_PATIENCE = 5 56 | MIN_DELTA = .0005 57 | PRINT_MODEL_SUMMARY = True #print layers and weights to stdout 58 | OPTIMIZER = None #adam, sgd, rmsprop, etc.. None accepts default 59 | LEARNING_RATE = 0.001 #only used when OPTIMIZER specified 60 | LEARNING_RATE_DECAY = 0.0 #only used when OPTIMIZER specified 61 | PRUNE_CNN = False 62 | PRUNE_PERCENT_TARGET = 75 # The desired percentage of pruning. 63 | PRUNE_PERCENT_PER_ITERATION = 20 # Percenge of pruning that is perform per iteration. 64 | PRUNE_VAL_LOSS_DEGRADATION_LIMIT = 0.2 # The max amout of validation loss that is permitted during pruning. 65 | PRUNE_EVAL_PERCENT_OF_DATASET = .05 # percent of dataset used to perform evaluation of model. 66 | 67 | #model transfer options 68 | FREEZE_LAYERS = False 69 | NUM_LAST_LAYERS_TO_TRAIN = 7 70 | 71 | #For the categorical model, this limits the upper bound of the learned throttle 72 | #it's very IMPORTANT that this value is matched from the training PC config.py and the robot.py 73 | #and ideally wouldn't change once set. 74 | MODEL_CATEGORICAL_MAX_THROTTLE_RANGE = 0.8 75 | 76 | #RNN or 3D 77 | SEQUENCE_LENGTH = 3 78 | 79 | #SOMBRERO 80 | HAVE_SOMBRERO = False 81 | 82 | #RECORD OPTIONS 83 | RECORD_DURING_AI = False 84 | -------------------------------------------------------------------------------- /donkeycar/templates/just_drive.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Scripts to drive a donkey 2 car 4 | 5 | Usage: 6 | manage.py (drive) 7 | 8 | Options: 9 | -h --help Show this screen. 10 | """ 11 | import os 12 | import time 13 | 14 | from docopt import docopt 15 | 16 | import donkeycar as dk 17 | from donkeycar.parts.datastore import TubHandler 18 | from donkeycar.parts.actuator import PCA9685, PWMSteering, PWMThrottle 19 | 20 | 21 | def drive(cfg): 22 | ''' 23 | Construct a working robotic vehicle from many parts. 24 | Each part runs as a job in the Vehicle loop, calling either 25 | it's run or run_threaded method depending on the constructor flag `threaded`. 26 | All parts are updated one after another at the framerate given in 27 | cfg.DRIVE_LOOP_HZ assuming each part finishes processing in a timely manner. 28 | Parts may have named outputs and inputs. The framework handles passing named outputs 29 | to parts requesting the same named input. 30 | ''' 31 | 32 | #Initialize car 33 | V = dk.vehicle.Vehicle() 34 | 35 | class MyController: 36 | ''' 37 | a simple controller class that outputs a constant steering and throttle. 38 | ''' 39 | def run(self): 40 | steering = 0.0 41 | throttle = 0.1 42 | return steering, throttle 43 | 44 | V.add(MyController(), outputs=['angle', 'throttle']) 45 | 46 | #Drive train setup 47 | steering_controller = PCA9685(cfg.STEERING_CHANNEL, cfg.PCA9685_I2C_ADDR, busnum=cfg.PCA9685_I2C_BUSNUM) 48 | steering = PWMSteering(controller=steering_controller, 49 | left_pulse=cfg.STEERING_LEFT_PWM, 50 | right_pulse=cfg.STEERING_RIGHT_PWM) 51 | 52 | throttle_controller = PCA9685(cfg.THROTTLE_CHANNEL, cfg.PCA9685_I2C_ADDR, busnum=cfg.PCA9685_I2C_BUSNUM) 53 | throttle = PWMThrottle(controller=throttle_controller, 54 | max_pulse=cfg.THROTTLE_FORWARD_PWM, 55 | zero_pulse=cfg.THROTTLE_STOPPED_PWM, 56 | min_pulse=cfg.THROTTLE_REVERSE_PWM) 57 | 58 | V.add(steering, inputs=['angle']) 59 | V.add(throttle, inputs=['throttle']) 60 | 61 | #run the vehicle for 20 seconds 62 | V.start(rate_hz=cfg.DRIVE_LOOP_HZ, 63 | max_loop_count=cfg.MAX_LOOPS) 64 | 65 | 66 | if __name__ == '__main__': 67 | args = docopt(__doc__) 68 | cfg = dk.load_config() 69 | 70 | if args['drive']: 71 | drive(cfg) 72 | -------------------------------------------------------------------------------- /donkeycar/parts/odometer.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Tuple 3 | 4 | from donkeycar.utilities.circular_buffer import CircularBuffer 5 | 6 | 7 | class Odometer: 8 | """ 9 | An Odometer takes the output of a Tachometer (revolutions) and 10 | turns those into a distance and velocity. Velocity can be 11 | optionally smoothed across a given number of readings. 12 | """ 13 | def __init__(self, distance_per_revolution:float, smoothing_count=1, debug=False): 14 | self.distance_per_revolution:float = distance_per_revolution 15 | self.timestamp:float = 0 16 | self.revolutions:float = 0 17 | self.running:bool = True 18 | self.queue = CircularBuffer(smoothing_count if smoothing_count >= 1 else 1) 19 | self.debug = debug 20 | self.reading = (0, 0, None) # distance, velocity, timestamp 21 | 22 | def poll(self, revolutions:int, timestamp:float=None): 23 | if self.running: 24 | if timestamp is None: 25 | timestamp = time.time() 26 | distance = revolutions * self.distance_per_revolution 27 | 28 | # smooth velocity 29 | velocity = 0 30 | if self.queue.count > 0: 31 | lastDistance, lastVelocity, lastTimestamp = self.queue.tail() 32 | if timestamp > lastTimestamp: 33 | velocity = (distance - lastDistance) / (timestamp - lastTimestamp) 34 | self.queue.enqueue((distance, velocity, timestamp)) 35 | 36 | # 37 | # Assignment in Python is atomic and so it is threadsafe 38 | # 39 | self.reading = (distance, velocity, timestamp) 40 | 41 | def update(self): 42 | while(self.running): 43 | self.poll(self.revolutions, None) 44 | time.sleep(0) # give other threads time 45 | 46 | def run_threaded(self, revolutions:float=0, timestamp:float=None) -> Tuple[float, float, float]: 47 | if self.running: 48 | self.revolutions = revolutions 49 | self.timestamp = timestamp if timestamp is not None else time.time() 50 | 51 | return self.reading 52 | return 0, 0, self.timestamp 53 | 54 | def run(self, revolutions:float=0, timestamp:float=None) -> Tuple[float, float, float]: 55 | if self.running: 56 | self.timestamp = timestamp if timestamp is not None else time.time() 57 | self.poll(revolutions, self.timestamp) 58 | 59 | return self.reading 60 | return 0, 0, self.timestamp 61 | 62 | def shutdown(self): 63 | self.running = False 64 | -------------------------------------------------------------------------------- /donkeycar/utilities/deprecated.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import inspect 3 | import warnings 4 | 5 | string_types = (type(b''), type(u'')) 6 | 7 | 8 | def deprecated(reason): 9 | """ 10 | This is a decorator which can be used to mark functions 11 | or classes as deprecated. It will result in a warning 12 | being emitted when the function is used. 13 | Taken from this fantastic answer by Laurent LaPorte: 14 | https://stackoverflow.com/a/40301488/1733315 15 | """ 16 | 17 | if isinstance(reason, string_types): 18 | 19 | # The @deprecated is used with a 'reason'. 20 | # 21 | # .. code-block:: python 22 | # 23 | # @deprecated("please, use another function") 24 | # def old_function(x, y): 25 | # pass 26 | 27 | def decorator(func1): 28 | 29 | if inspect.isclass(func1): 30 | fmt1 = "Call to deprecated class {name} ({reason})." 31 | else: 32 | fmt1 = "Call to deprecated function {name} ({reason})." 33 | 34 | @functools.wraps(func1) 35 | def new_func1(*args, **kwargs): 36 | warnings.simplefilter('always', DeprecationWarning) 37 | warnings.warn( 38 | fmt1.format(name=func1.__name__, reason=reason), 39 | category=DeprecationWarning, 40 | stacklevel=2 41 | ) 42 | warnings.simplefilter('default', DeprecationWarning) 43 | return func1(*args, **kwargs) 44 | 45 | return new_func1 46 | 47 | return decorator 48 | 49 | elif inspect.isclass(reason) or inspect.isfunction(reason): 50 | 51 | # The @deprecated is used without any 'reason'. 52 | # 53 | # .. code-block:: python 54 | # 55 | # @deprecated 56 | # def old_function(x, y): 57 | # pass 58 | 59 | func2 = reason 60 | 61 | if inspect.isclass(func2): 62 | fmt2 = "Call to deprecated class {name}." 63 | else: 64 | fmt2 = "Call to deprecated function {name}." 65 | 66 | @functools.wraps(func2) 67 | def new_func2(*args, **kwargs): 68 | warnings.simplefilter('always', DeprecationWarning) 69 | warnings.warn( 70 | fmt2.format(name=func2.__name__), 71 | category=DeprecationWarning, 72 | stacklevel=2 73 | ) 74 | warnings.simplefilter('default', DeprecationWarning) 75 | return func2(*args, **kwargs) 76 | 77 | return new_func2 78 | 79 | else: 80 | raise TypeError(repr(type(reason))) 81 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = donkeycar 3 | version = attr: donkeycar.__version__ 4 | author = Will Roscoe, Adam Conway, Tawn Kramer 5 | url = https://github.com/autorope/donkeycar 6 | description = Self driving library for python. 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | keywords = selfdriving cars donkeycar diyrobocars 10 | license = MIT 11 | classifiers = 12 | # How mature is this project? Common values are 13 | # 3 - Alpha 14 | # 4 - Beta 15 | # 5 - Production/Stable 16 | Development Status :: 4 - Beta 17 | # Indicate who your project is intended for 18 | Intended Audience :: Developers 19 | Topic :: Scientific/Engineering :: Artificial Intelligence 20 | Programming Language :: Python :: 3.11 21 | 22 | [options] 23 | packages = find_namespace: 24 | zip_safe = True 25 | include_package_data = True 26 | python_requires = >=3.11.0,<3.12 27 | install_requires = 28 | numpy 29 | pillow 30 | docopt 31 | tornado 32 | requests 33 | PrettyTable 34 | paho-mqtt 35 | simple_pid 36 | progress 37 | pyfiglet 38 | psutil 39 | pynmea2 40 | pyserial 41 | utm 42 | pandas 43 | pyyaml 44 | 45 | [options.extras_require] 46 | pi = 47 | picamera2 48 | Adafruit_PCA9685 49 | adafruit-circuitpython-ssd1306 50 | adafruit-circuitpython-rplidar 51 | RPi.GPIO 52 | flatbuffers==24.3.* 53 | tensorflow-aarch64==2.15.* 54 | opencv-contrib-python 55 | matplotlib 56 | kivy 57 | kivy-garden.matplotlib 58 | pandas 59 | plotly 60 | albumentations 61 | 62 | nano = 63 | Adafruit_PCA9685 64 | adafruit-circuitpython-ssd1306 65 | adafruit-circuitpython-rplidar 66 | Jetson.GPIO 67 | numpy==1.23.* 68 | matplotlib==3.7.* 69 | kivy 70 | kivy-garden.matplotlib 71 | plotly 72 | pandas==2.0.* 73 | 74 | pc = 75 | tensorflow==2.15.* 76 | matplotlib 77 | kivy 78 | kivy-garden.matplotlib 79 | pandas 80 | plotly 81 | albumentations 82 | 83 | macos = 84 | tensorflow==2.15.* 85 | tensorflow-metal 86 | matplotlib 87 | kivy 88 | kivy-garden.matplotlib 89 | pandas 90 | plotly 91 | albumentations 92 | 93 | dev = 94 | pytest 95 | pytest-cov 96 | responses 97 | mypy 98 | 99 | torch = 100 | torch==2.1.* 101 | pytorch-lightning 102 | torchvision 103 | torchaudio 104 | fastai 105 | 106 | 107 | [options.package_data] 108 | * = *.html, *.ini, *.txt, *.kv 109 | 110 | [options.entry_points] 111 | console_scripts = 112 | donkey = donkeycar.management.base:execute_from_command_line 113 | -------------------------------------------------------------------------------- /donkeycar/tests/test_tub_v2.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import tempfile 3 | import unittest 4 | 5 | from donkeycar.parts.tub_v2 import Tub 6 | from donkeycar.pipeline.types import TubRecord, Collator 7 | from donkeycar.config import Config 8 | 9 | 10 | class TestTub(unittest.TestCase): 11 | _path = None 12 | delete_indexes = [3, 8] 13 | 14 | @classmethod 15 | def setUpClass(cls) -> None: 16 | cls._path = tempfile.mkdtemp() 17 | inputs = ['input'] 18 | types = ['int'] 19 | cls.tub = Tub(cls._path, inputs, types) 20 | 21 | def test_basic_tub_operations(self): 22 | entries = list(self.tub) 23 | self.assertEqual(len(entries), 0) 24 | write_count = 14 25 | 26 | records = [{'input': i} for i in range(write_count)] 27 | for record in records: 28 | self.tub.write_record(record) 29 | 30 | for index in self.delete_indexes: 31 | self.tub.delete_records(index) 32 | 33 | count = 0 34 | for record in self.tub: 35 | print(f'Record {record}') 36 | count += 1 37 | 38 | self.assertEqual(count, (write_count - len(self.delete_indexes))) 39 | self.assertEqual(len(self.tub), 40 | (write_count - len(self.delete_indexes))) 41 | 42 | def test_sequence(self): 43 | cfg = Config() 44 | records = [TubRecord(cfg, self.tub.base_path, underlying) for 45 | underlying in self.tub] 46 | for seq_len in (2, 3, 4, 5): 47 | seq = Collator(seq_len, records) 48 | for l in seq: 49 | print(l) 50 | assert len(l) == seq_len, 'Sequence has wrong length' 51 | assert not any((r.underlying['_index'] == del_idx for del_idx in 52 | self.delete_indexes for r in l)), \ 53 | 'Deleted index found' 54 | it1 = iter(l) 55 | it2 = iter(l) 56 | next(it2) 57 | assert all((Collator.is_continuous(rec_1, rec_2) 58 | for rec_1, rec_2 in zip(it1, it2))), \ 59 | 'Non continuous records found' 60 | 61 | def test_delete_last_n_records(self): 62 | start_len = len(self.tub) 63 | self.tub.delete_last_n_records(2) 64 | self.assertEqual(start_len - 2, len(self.tub), 65 | "error in deleting 2 last records") 66 | self.tub.delete_last_n_records(3) 67 | self.assertEqual(start_len - 5, len(self.tub), 68 | "error in deleting 3 last records") 69 | 70 | @classmethod 71 | def tearDownClass(cls) -> None: 72 | shutil.rmtree(cls._path) 73 | 74 | 75 | if __name__ == '__main__': 76 | unittest.main() 77 | -------------------------------------------------------------------------------- /donkeycar/parts/voice_control/alexa.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | 4 | 5 | class AlexaController(object): 6 | ''' 7 | Accept simple command from alexa. For the command supported, please refer 8 | to the README.md 9 | ''' 10 | API_ENDPOINT = "http://alexa.robocarstore.com" 11 | 12 | def __init__(self, ctr, cfg, debug=False): 13 | self.running = True 14 | self.debug = debug 15 | self.ctr = ctr 16 | self.cfg = cfg # Pass the config object for altering AI_THROTTLE_MULT 17 | self.DEFAULT_AI_THROTTLE_MULT = self.cfg.AI_THROTTLE_MULT 18 | 19 | if self.cfg.ALEXA_DEVICE_CODE is None: 20 | raise Exception("Please set cfg.ALEXA_DEVICE_CODE in myconfig.py") 21 | 22 | def log(self, msg): 23 | print("Voice control: {}".format(msg)) 24 | 25 | def get_command(self): 26 | url = "{}/{}".format(self.API_ENDPOINT, 'command') 27 | 28 | params = { 29 | 'deviceCode': self.cfg.ALEXA_DEVICE_CODE 30 | } 31 | 32 | command = None 33 | 34 | try: 35 | response = requests.get(url=url, params=params, timeout=5) 36 | response.raise_for_status() 37 | result = response.json() 38 | if "command" in result: 39 | command = result['command'] 40 | else: 41 | self.log("Warning - No command found in response") 42 | except requests.exceptions.RequestException as e: 43 | self.log("Warning - Failed to reach Alexa API endpoint") 44 | except ValueError: # Catch JSONDecodeError 45 | self.log('Warning - Decoding JSON failed') 46 | 47 | return command 48 | 49 | def update(self): 50 | while (self.running): 51 | command = self.get_command() 52 | if self.debug: 53 | self.log("Command = {}".format(command)) 54 | elif command is not None: 55 | self.log("Command = {}".format(command)) 56 | 57 | if command == "autopilot": 58 | self.ctr.mode = "local" 59 | elif command == "speedup": 60 | self.cfg.AI_THROTTLE_MULT += 0.05 61 | elif command == "slowdown": 62 | self.cfg.AI_THROTTLE_MULT -= 0.05 63 | elif command == "stop": 64 | self.ctr.mode = "user" 65 | self.cfg.AI_THROTTLE_MULT = self.DEFAULT_AI_THROTTLE_MULT 66 | 67 | if self.debug: 68 | self.log("mode = {}, cfg.AI_THROTTLE_MULT={}".format( 69 | self.ctr.mode, self.cfg.AI_THROTTLE_MULT)) 70 | 71 | time.sleep(0.25) # Give a break between requests 72 | 73 | def run_threaded(self): 74 | pass 75 | 76 | def shutdown(self): 77 | self.running = False 78 | -------------------------------------------------------------------------------- /donkeycar/tests/test_datastore_v2.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tempfile 4 | import time 5 | import unittest 6 | from pathlib import Path 7 | 8 | from donkeycar.parts.datastore_v2 import Manifest 9 | 10 | 11 | class TestDatastore(unittest.TestCase): 12 | 13 | def setUp(self): 14 | self._path = tempfile.mkdtemp() 15 | 16 | def test_basic_datastore_operations(self): 17 | # 2 records per catalog entry in the manifest 18 | manifest = Manifest(self._path, max_len=2) 19 | count = 10 20 | for i in range(count): 21 | manifest.write_record(self._newRecord()) 22 | 23 | read_records = 0 24 | for entry in manifest: 25 | print('Entry %s' % (entry)) 26 | read_records += 1 27 | 28 | self.assertEqual(count, read_records) 29 | 30 | def test_deletion(self): 31 | manifest = Manifest(self._path, max_len=2) 32 | count = 10 33 | deleted = 5 34 | for i in range(count): 35 | manifest.write_record(self._newRecord()) 36 | 37 | for i in range(deleted): 38 | manifest.delete_records(i) 39 | 40 | read_records = 0 41 | for entry in manifest: 42 | print(f'Entry {entry}') 43 | read_records += 1 44 | 45 | self.assertEqual((count - deleted), read_records) 46 | 47 | def test_delete_and_restore_by_set(self): 48 | manifest = Manifest(self._path, max_len=2) 49 | count = 10 50 | deleted = range(3, 7) 51 | for i in range(count): 52 | manifest.write_record(self._newRecord()) 53 | 54 | manifest.delete_records(deleted) 55 | read_records = 0 56 | for entry in manifest: 57 | print(f'Entry {entry}') 58 | read_records += 1 59 | 60 | self.assertEqual(count - len(deleted), read_records) 61 | 62 | manifest.restore_records(deleted) 63 | read_records = 0 64 | for entry in manifest: 65 | print(f'Entry {entry}') 66 | read_records += 1 67 | 68 | self.assertEqual(count, read_records) 69 | 70 | def test_memory_mapped_read(self): 71 | manifest = Manifest(self._path, max_len=2) 72 | for i in range(10): 73 | manifest.write_record(self._newRecord()) 74 | manifest.close() 75 | 76 | manifest_2 = Manifest(self._path, read_only=True) 77 | read_records = 0 78 | for _ in manifest_2: 79 | read_records += 1 80 | 81 | manifest_2.close() 82 | 83 | self.assertEqual(10, read_records) 84 | 85 | def tearDown(self): 86 | shutil.rmtree(self._path) 87 | 88 | def _newRecord(self): 89 | record = {'at' : time.time()} 90 | return record 91 | 92 | 93 | if __name__ == '__main__': 94 | unittest.main() 95 | -------------------------------------------------------------------------------- /donkeycar/gym/gym_real.py: -------------------------------------------------------------------------------- 1 | ''' 2 | file: gym_real.py 3 | author: Tawn Kramer 4 | date: 2019-01-24 5 | desc: Control a real donkey robot via the gym interface 6 | ''' 7 | import os 8 | import time 9 | 10 | import gym 11 | import numpy as np 12 | from gym import error, spaces, utils 13 | 14 | from .remote_controller import DonkeyRemoteContoller 15 | 16 | 17 | class DonkeyRealEnv(gym.Env): 18 | """ 19 | OpenAI Gym Environment for a real Donkey 20 | """ 21 | 22 | metadata = { 23 | "render.modes": ["human", "rgb_array"], 24 | } 25 | 26 | ACTION_NAMES = ["steer", "throttle"] 27 | STEER_LIMIT_LEFT = -1.0 28 | STEER_LIMIT_RIGHT = 1.0 29 | THROTTLE_MIN = 0.0 30 | THROTTLE_MAX = 5.0 31 | VAL_PER_PIXEL = 255 32 | 33 | def __init__(self, time_step=0.05, frame_skip=2): 34 | 35 | print("starting DonkeyGym env") 36 | 37 | try: 38 | donkey_name = str(os.environ['DONKEY_NAME']) 39 | except: 40 | donkey_name = 'my_robot1234' 41 | print("No DONKEY_NAME environment var. Using default:", donkey_name) 42 | 43 | try: 44 | mqtt_broker = str(os.environ['DONKEY_MQTT_BROKER']) 45 | except: 46 | mqtt_broker = "iot.eclipse.org" 47 | print("No DONKEY_MQTT_BROKER environment var. Using default:", mqtt_broker) 48 | 49 | # start controller 50 | self.controller = DonkeyRemoteContoller(donkey_name=donkey_name, mqtt_broker=mqtt_broker) 51 | 52 | # steering and throttle 53 | self.action_space = spaces.Box(low=np.array([self.STEER_LIMIT_LEFT, self.THROTTLE_MIN]), 54 | high=np.array([self.STEER_LIMIT_RIGHT, self.THROTTLE_MAX]), dtype=np.float32 ) 55 | 56 | # camera sensor data 57 | self.observation_space = spaces.Box(0, self.VAL_PER_PIXEL, self.controller.get_sensor_size(), dtype=np.uint8) 58 | 59 | # Frame Skipping 60 | self.frame_skip = frame_skip 61 | 62 | # wait until loaded 63 | self.controller.wait_until_connected() 64 | 65 | 66 | def close(self): 67 | self.controller.quit() 68 | 69 | def step(self, action): 70 | for i in range(self.frame_skip): 71 | self.controller.take_action(action) 72 | time.sleep(0.05) 73 | observation = self.controller.observe() 74 | reward, done, info = 0.1, False, None 75 | return observation, reward, done, info 76 | 77 | def reset(self): 78 | observation = self.controller.observe() 79 | reward, done, info = 0.1, False, None 80 | return observation 81 | 82 | def render(self, mode="human", close=False): 83 | if close: 84 | self.controller.quit() 85 | 86 | return self.controller.observe() 87 | 88 | def is_game_over(self): 89 | return False 90 | -------------------------------------------------------------------------------- /donkeycar/tests/test_scripts.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import tarfile 4 | 5 | from donkeycar import utils 6 | import pytest 7 | 8 | 9 | def is_error(err): 10 | for e in err: 11 | # Catch error if 'Error' is in the stderr output. 12 | if 'Error' in e.decode(): 13 | return True 14 | # Catch error when the wrong command is used. 15 | if 'Usage:' in e.decode(): 16 | return True 17 | return False 18 | 19 | 20 | @pytest.fixture 21 | def cardir(tmpdir_factory): 22 | path = tmpdir_factory.mktemp('mycar') 23 | return path 24 | 25 | 26 | def test_createcar(cardir): 27 | cmd = ['donkey', 'createcar', '--path', cardir] 28 | out, err, proc_id = utils.run_shell_command(cmd) 29 | assert is_error(err) is False 30 | 31 | 32 | def test_drivesim(cardir): 33 | cmd = ['donkey', 'createcar', '--path', cardir ,'--template', 'square'] 34 | out, err, proc_id = utils.run_shell_command(cmd, timeout=10) 35 | cmd = ['python', 'manage.py', 'drive'] 36 | out, err, proc_id = utils.run_shell_command(cmd, cwd=cardir) 37 | print(err) 38 | 39 | if is_error(err) is True: 40 | print('out', out) 41 | print('error: ', err) 42 | raise ValueError(err) 43 | 44 | 45 | def test_bad_command_fails(): 46 | cmd = ['donkey', 'not a comand'] 47 | out, err, proc_id = utils.run_shell_command(cmd) 48 | print(err) 49 | print(out) 50 | assert is_error(err) is True 51 | 52 | 53 | def test_tubplot(cardir): 54 | # create empy KerasLinear model in car directory 55 | model_dir = os.path.join(cardir, 'models') 56 | os.mkdir(model_dir) 57 | model_path = os.path.join(model_dir, 'model.savedmodel') 58 | from donkeycar.parts.keras import KerasLinear 59 | KerasLinear().interpreter.model.save(model_path) 60 | 61 | # extract tub.tar.gz into car_dir/tub 62 | this_dir = os.path.dirname(os.path.abspath(__file__)) 63 | with tarfile.open(os.path.join(this_dir, 'tub', 'tub.tar.gz')) as file: 64 | file.extractall(cardir) 65 | # define the tub dir 66 | tub_dir = os.path.join(cardir, 'tub') 67 | # put a dummy config file into the car dir 68 | cfg_file = os.path.join(cardir, 'config.py') 69 | with open(cfg_file, "w+") as f: 70 | f.writelines(["# config file\n", "IMAGE_H = 120\n", "IMAGE_W = 160\n", 71 | "IMAGE_DEPTH = 3\n", "\n"]) 72 | cmd = ['donkey', 'tubplot', '--tub', tub_dir, '--model', model_path, 73 | '--type', 'linear', '--noshow'] 74 | 75 | # run donkey command in subprocess 76 | with subprocess.Popen(cmd, cwd=cardir, stdout=subprocess.PIPE) as pipe: 77 | line = '\nStart test: \n' 78 | while line: 79 | print(line, end='') 80 | line = pipe.stdout.readline().decode() 81 | print(f'List model dir: {os.listdir(model_dir)}') 82 | assert os.path.exists(model_path + '_pred.png') 83 | 84 | -------------------------------------------------------------------------------- /donkeycar/parts/web_controller/templates/static/style.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 768px) { 2 | #joystick-column { 3 | /* This causes joystick to be offscreen on iPhone 5s, 6, iPad 2 4 | position: fixed; 5 | bottom:100; 6 | right:0;*/ 7 | width:100%; 8 | height:0; 9 | 10 | } 11 | 12 | #joystick-padding { 13 | height:220px; 14 | } 15 | } 16 | 17 | /* override bootstrap */ 18 | form { 19 | margin-bottom: 0.5em; 20 | } 21 | .form-control { 22 | display: inline; 23 | width: auto; 24 | padding: 0.25em 0.25em 25 | } 26 | 27 | #button_bar button { 28 | display: inline-block; 29 | padding-left: 1.5em; 30 | padding-right: 1.5em; 31 | } 32 | 33 | #controls-column { 34 | margin-top:10px; 35 | } 36 | #control-bars { 37 | margin-top:10px; 38 | } 39 | .group-label { 40 | width:auto; 41 | } 42 | 43 | #control-bars > div { 44 | display: inline; 45 | } 46 | 47 | /* this setting is too large on iPhone5s */ 48 | #control-bars .progress { 49 | width: 15%; 50 | } 51 | 52 | #control-bars .glyphicon { 53 | margin-left: 1.5em; 54 | margin-right: 0.5em; 55 | } 56 | 57 | #control-bars .progress.negative { 58 | float: left; 59 | border-top-right-radius: 0px; 60 | border-bottom-right-radius: 0px; 61 | } 62 | 63 | #control-bars .progress.positive { 64 | float: left; 65 | border-top-left-radius: 0px; 66 | border-bottom-left-radius: 0px; 67 | } 68 | 69 | #vehicle_controls input { 70 | width: 100%; 71 | } 72 | 73 | #mpeg-image { 74 | width:100%; 75 | } 76 | 77 | #joystick_container { 78 | text-align: center; 79 | width: 100%; 80 | height: 17em; 81 | padding-top: 8em; 82 | border-color: #bce8f1; 83 | background-color: #d9edf7; 84 | } 85 | 86 | #vehicle_footer { 87 | height:auto; 88 | } 89 | 90 | #brake_button { 91 | margin-top:5px; 92 | margin-bottom: 5px; 93 | } 94 | 95 | div.session-thumbnails .ui-selecting { 96 | background-color: #fcf8e3; 97 | border-color: #faebcc; 98 | } 99 | 100 | div.session-thumbnails .ui-selected { 101 | background-color: #f2dede; 102 | border-color: #ebccd1; 103 | } 104 | 105 | div.session-thumbnails img { 106 | width: 160px; 107 | height: auto; 108 | } 109 | 110 | div.session-thumbnails .desc { 111 | margin-bottom:0px; 112 | } 113 | 114 | div.session-thumbnails li.thumbnail { 115 | width:170px; 116 | float: left; 117 | margin-right:5px; 118 | } 119 | 120 | div.session-thumbnails .caption { 121 | padding:5px 5px 0px 5px; 122 | } 123 | 124 | div.session-thumbnails label { 125 | padding-left: 0px; 126 | } 127 | 128 | div.img-controls { 129 | margin: 20px 0; 130 | } 131 | 132 | body { 133 | /* Margin bottom by footer height */ 134 | margin-bottom: 60px; 135 | } 136 | .footer { 137 | position: fixed; 138 | bottom: 0; 139 | width: 100%; 140 | /* Set the fixed height of the footer here */ 141 | height: 60px; 142 | background-color: #f5f5f5; 143 | } -------------------------------------------------------------------------------- /donkeycar/parts/fast_stretch.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | from pathlib import Path 4 | import time 5 | 6 | Mx = 128 # Natural mean 7 | C = 0.007 # Base line fraction 8 | Ts = 0.02 # Tunable amplitude 9 | Tr = 0.7 # Threshold 10 | T = -0.3 # Gamma boost 11 | Epsilon = 1e-07 # Epsilon 12 | 13 | 14 | def fast_stretch(image, debug=False): 15 | hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) 16 | (h, s, v) = cv2.split(hsv) 17 | input = v 18 | shape = input.shape 19 | rows = shape[0] 20 | cols = shape[1] 21 | size = rows * cols 22 | output = np.empty_like(input) 23 | if debug: 24 | start = time.time() 25 | mean = np.mean(input) 26 | t = (mean - Mx) / Mx 27 | Sl = 0. 28 | Sh = 0. 29 | if t <= 0: 30 | Sl = C 31 | Sh = C - (Ts * t) 32 | else: 33 | Sl = C + (Ts * t) 34 | Sh = C 35 | 36 | gamma = 1. 37 | if t <= T: 38 | gamma = max((1 + (t - T)), Tr) 39 | 40 | if debug: 41 | time_taken = (time.time() - start) * 1000 42 | print('Preprocessing time %s' % time_taken) 43 | start = time.time() 44 | 45 | histogram = cv2.calcHist([input], [0], None, [256], [0, 256]) 46 | # Walk histogram 47 | Xl = 0 48 | Xh = 255 49 | targetFl = Sl * size 50 | targetFh = Sh * size 51 | 52 | count = histogram[Xl] 53 | while count < targetFl: 54 | count += histogram[Xl] 55 | Xl += 1 56 | 57 | count = histogram[Xh] 58 | while count < targetFh: 59 | count += histogram[Xh] 60 | Xh -= 1 61 | 62 | if debug: 63 | time_taken = (time.time() - start) * 1000 64 | print('Histogram Binning %s' % time_taken) 65 | start = time.time() 66 | 67 | # Vectorized ops 68 | output = np.where(input <= Xl, 0, input) 69 | output = np.where(output >= Xh, 255, output) 70 | output = np.where(np.logical_and(output > Xl, output < Xh), np.multiply( 71 | 255, np.power(np.divide(np.subtract(output, Xl), np.max([np.subtract(Xh, Xl), Epsilon])), gamma)), output) 72 | # max to 255 73 | output = np.where(output > 255., 255., output) 74 | output = np.asarray(output, dtype='uint8') 75 | output = cv2.merge((h, s, output)) 76 | output = cv2.cvtColor(output, cv2.COLOR_HSV2RGB) 77 | 78 | if debug: 79 | time_taken = (time.time() - start) * 1000 80 | print('Vector Ops %s' % time_taken) 81 | start = time.time() 82 | 83 | return output 84 | 85 | 86 | if __name__ == "__main__": 87 | path = Path('images/Lenna.jpg') 88 | image = cv2.imread(path.as_posix()) 89 | # Ensure BGR 90 | bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) 91 | image_data = np.asarray(bgr, dtype=np.uint8) 92 | 93 | stretched = fast_stretch(image_data, debug=True) 94 | cv2.imshow('Original', image) 95 | cv2.imshow('Contrast Stretched', stretched) 96 | cv2.waitKey(0) 97 | cv2.destroyAllWindows() 98 | -------------------------------------------------------------------------------- /donkeycar/tests/test_lidar.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def has_lidar(): 5 | """ Determine if test platform (nano, pi) has lidar""" 6 | # for now just return false, better to use an environ 7 | return False 8 | 9 | 10 | @pytest.mark.skipif(not has_lidar(), reason='Need lidar installed') 11 | def test_rp_lidar(): 12 | from rplidar import RPLidar 13 | import serial 14 | import glob 15 | 16 | temp_list = glob.glob('/dev/ttyUSB*') 17 | result = [] 18 | for a_port in temp_list: 19 | try: 20 | s = serial.Serial(a_port) 21 | s.close() 22 | result.append(a_port) 23 | except serial.SerialException: 24 | pass 25 | print("available ports", result) 26 | lidar = RPLidar(result[0], baudrate=115200) 27 | 28 | info = lidar.get_info() 29 | print(info) 30 | 31 | health = lidar.get_health() 32 | print(health) 33 | 34 | for i, scan in enumerate(lidar.iter_scans()): 35 | print(f'{i}: Got {len(scan)} measurements') 36 | if i > 10: 37 | break 38 | 39 | lidar.stop() 40 | lidar.stop_motor() 41 | lidar.disconnect() 42 | 43 | 44 | @pytest.mark.skipif(not has_lidar(), reason='Need lidar installed') 45 | def test_py_lidar3(): 46 | import PyLidar3 47 | import serial 48 | import glob 49 | import time # Time module 50 | temp_list = glob.glob ('/dev/ttyUSB*') 51 | result = [] 52 | for a_port in temp_list: 53 | try: 54 | s = serial.Serial(a_port) 55 | s.close() 56 | result.append(a_port) 57 | except serial.SerialException: 58 | pass 59 | print("available ports", result) 60 | 61 | port = result[0] # linux 62 | # PyLidar3.your_version_of_lidar(port,chunk_size) 63 | ydlidar = PyLidar3.YdLidarX4(port) 64 | if ydlidar.Connect(): 65 | print(ydlidar.GetDeviceInfo()) 66 | gen = ydlidar.StartScanning() 67 | t = time.time() # start time 68 | while (time.time() - t) < 30: # scan for 30 seconds 69 | print(next(gen)) 70 | time.sleep(0.5) 71 | ydlidar.StopScanning() 72 | ydlidar.Disconnect() 73 | else: 74 | print("Error connecting to device") 75 | 76 | 77 | @pytest.mark.skipif(not has_lidar(), reason='Need lidar installed') 78 | def test_simple_express_scan(): 79 | from pyrplidar import PyRPlidar 80 | import time 81 | lidar = PyRPlidar() 82 | lidar.connect(port="/dev/ttyUSB0", baudrate=115200, timeout=3) 83 | # Linux : "/dev/ttyUSB0" 84 | # MacOS : "/dev/cu.SLAB_USBtoUART" 85 | # Windows : "COM5" 86 | 87 | lidar.set_motor_pwm(500) 88 | time.sleep(2) 89 | scan_generator = lidar.start_scan_express(4) 90 | 91 | for count, scan in enumerate(scan_generator()): 92 | print(count, scan) 93 | if count == 20: break 94 | 95 | lidar.stop() 96 | lidar.set_motor_pwm(0) 97 | lidar.disconnect() 98 | -------------------------------------------------------------------------------- /donkeycar/tests/test_keras.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import tempfile 3 | from pytest import approx 4 | import pytest 5 | import os 6 | 7 | from donkeycar.parts.interpreter import keras_to_tflite, \ 8 | saved_model_to_tensor_rt, TfLite, TensorRT, has_trt_support 9 | from donkeycar.parts.keras import * 10 | from donkeycar.utils import get_test_img 11 | 12 | TOLERANCE = 1e-4 13 | 14 | 15 | @pytest.fixture 16 | def tmp_dir() -> str: 17 | tmp_dir = tempfile.mkdtemp() 18 | yield tmp_dir 19 | shutil.rmtree(tmp_dir) 20 | 21 | 22 | test_data = [KerasLinear, KerasCategorical, KerasInferred, KerasLSTM, 23 | KerasLocalizer, KerasIMU, Keras3D_CNN, KerasMemory, 24 | KerasBehavioral] 25 | 26 | 27 | def create_models(keras_pilot, dir): 28 | # build with keras interpreter 29 | interpreter = KerasInterpreter() 30 | km = keras_pilot(interpreter=interpreter) 31 | # build tflite model from TfLite interpreter 32 | tflite_model_path = os.path.join(dir, 'model.tflite') 33 | keras_to_tflite(interpreter.model, tflite_model_path) 34 | kl = keras_pilot(interpreter=TfLite()) 35 | kl.load(tflite_model_path) 36 | # save model in savedmodel format 37 | savedmodel_path = os.path.join(dir, 'model.savedmodel') 38 | interpreter.model.save(savedmodel_path) 39 | krt = None 40 | # load tensorrt only if supported 41 | if has_trt_support(): 42 | krt = keras_pilot(interpreter=TensorRT()) 43 | krt.load(savedmodel_path) 44 | 45 | return km, kl, krt 46 | 47 | 48 | @pytest.mark.parametrize('keras_pilot', test_data) 49 | def test_keras_vs_tflite_and_tensorrt(keras_pilot, tmp_dir): 50 | """ This test cannot run for the 3D CNN model in tflite and the LSTM 51 | model in """ 52 | k_keras, k_tflite, k_trt = create_models(keras_pilot, tmp_dir) 53 | 54 | # prepare data 55 | img = get_test_img(k_keras) 56 | if keras_pilot is KerasIMU: 57 | # simulate 6 imu data in [0, 1] 58 | imu = np.random.rand(6).tolist() 59 | args = (img, imu) 60 | elif keras_pilot is KerasMemory: 61 | mem = np.random.rand(2).tolist() 62 | # steering should be in [-1, 1] 63 | mem[0] = mem[0] * 2 - 1 64 | args = (img, mem) 65 | elif keras_pilot is KerasBehavioral: 66 | one_hot = [1.0, 0.0] if np.random.rand() < 0.5 else [0.0, 1.0] 67 | args = (img, one_hot) 68 | else: 69 | args = (img, ) 70 | 71 | # run all three interpreters and check results are numerically close 72 | out2 = out3 = None 73 | out1 = k_keras.run(*args) 74 | if k_tflite: 75 | out2 = k_tflite.run(*args) 76 | assert out2 == approx(out1, rel=TOLERANCE, abs=TOLERANCE) 77 | if k_trt: 78 | # lstm cells are not yet supported in tensor RT 79 | out3 = k_trt.run(*args) 80 | assert out3 == approx(out1, rel=TOLERANCE, abs=TOLERANCE) 81 | print('keras:', out1, 'tflite:', out2, 'trt:', out3) 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /donkeycar/parts/pigpio_enc.py: -------------------------------------------------------------------------------- 1 | from donkeycar.utilities.deprecated import deprecated 2 | import pigpio 3 | import time 4 | 5 | # 6 | # contents of this file are deprecated 7 | # in favor of donkeycar.parts.tachometer and donkeycar.parts.odometer 8 | # 9 | 10 | @deprecated("Deprecated in favor of donkeycar.parts.odometer.Odometer") 11 | class OdomDist(object): 12 | """ 13 | Take a tick input from odometry and compute the distance travelled 14 | """ 15 | def __init__(self, mm_per_tick, debug=False): 16 | self.mm_per_tick = mm_per_tick 17 | self.m_per_tick = mm_per_tick / 1000.0 18 | self.meters = 0 19 | self.last_time = time.time() 20 | self.meters_per_second = 0 21 | self.debug = debug 22 | self.prev_ticks = 0 23 | 24 | def run(self, ticks, throttle): 25 | """ 26 | inputs => total ticks since start 27 | inputs => throttle, used to determine positive or negative vel 28 | return => total dist (m), current vel (m/s), delta dist (m) 29 | """ 30 | new_ticks = ticks - self.prev_ticks 31 | self.prev_ticks = ticks 32 | 33 | #save off the last time interval and reset the timer 34 | start_time = self.last_time 35 | end_time = time.time() 36 | self.last_time = end_time 37 | 38 | #calculate elapsed time and distance traveled 39 | seconds = end_time - start_time 40 | distance = new_ticks * self.m_per_tick 41 | if throttle < 0.0: 42 | distance = distance * -1.0 43 | velocity = distance / seconds 44 | 45 | #update the odometer values 46 | self.meters += distance 47 | self.meters_per_second = velocity 48 | 49 | #console output for debugging 50 | if(self.debug): 51 | print('seconds:', seconds) 52 | print('delta:', distance) 53 | print('velocity:', velocity) 54 | 55 | print('distance (m):', self.meters) 56 | print('velocity (m/s):', self.meters_per_second) 57 | 58 | return self.meters, self.meters_per_second, distance 59 | 60 | 61 | @deprecated("Deprecated in favor of donkeycar.parts.tachometer.GpioTachometer") 62 | class PiPGIOEncoder(): 63 | def __init__(self, pin, pi): 64 | self.pin = pin 65 | self.pi = pi 66 | self.pi.set_mode(pin, pigpio.INPUT) 67 | self.pi.set_pull_up_down(pin, pigpio.PUD_UP) 68 | self.cb = pi.callback(self.pin, pigpio.FALLING_EDGE, self._cb) 69 | self.count = 0 70 | 71 | def _cb(self, pin, level, tick): 72 | self.count += 1 73 | 74 | def run(self): 75 | return self.count 76 | 77 | def shutdown(self): 78 | if self.cb != None: 79 | self.cb.cancel() 80 | self.cb = None 81 | 82 | self.pi.stop() 83 | 84 | 85 | if __name__ == "__main__": 86 | pi = pigpio.pi() 87 | e = PiPGIOEncoder(4, pi) 88 | while True: 89 | time.sleep(0.1) 90 | e.run() 91 | 92 | 93 | -------------------------------------------------------------------------------- /donkeycar/management/tub_web/tub.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | {% block content %} 4 | 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | 22 |
23 |
24 |

0

25 | 31 | 34 | 37 | 40 | 43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | 52 | 53 | 54 |
55 |
56 |
57 |
58 | 61 |
62 |
63 |
64 | 65 | 66 | {% end %} 67 | -------------------------------------------------------------------------------- /donkeycar/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Wed Sep 13 21:27:44 2017 4 | 5 | @author: wroscoe 6 | """ 7 | import os 8 | import types 9 | import logging 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class Config: 15 | 16 | def from_pyfile(self, filename): 17 | d = types.ModuleType('config') 18 | d.__file__ = filename 19 | try: 20 | with open(filename, mode='rb') as config_file: 21 | exec(compile(config_file.read(), filename, 'exec'), d.__dict__) 22 | except IOError as e: 23 | e.strerror = 'Unable to load configuration file (%s)' % e.strerror 24 | raise 25 | self.from_object(d) 26 | return True 27 | 28 | def from_object(self, obj): 29 | for key in dir(obj): 30 | if key.isupper(): 31 | setattr(self, key, getattr(obj, key)) 32 | 33 | def from_dict(self, d, keys=[]): 34 | msg = 'Overwriting config with: ' 35 | for k, v in d.items(): 36 | if k.isupper(): 37 | if k in keys or not keys: 38 | setattr(self, k, v) 39 | msg += f'{k}:{v}, ' 40 | logger.info(msg) 41 | 42 | def __str__(self): 43 | result = [] 44 | for key in dir(self): 45 | if key.isupper(): 46 | result.append((key, getattr(self, key))) 47 | return str(result) 48 | 49 | def show(self): 50 | for attr in dir(self): 51 | if attr.isupper(): 52 | print(attr, ":", getattr(self, attr)) 53 | 54 | def to_pyfile(self, path): 55 | lines = [] 56 | for attr in dir(self): 57 | if attr.isupper(): 58 | v = getattr(self, attr) 59 | if isinstance(v, str): 60 | v = f'"{v}"' 61 | lines.append(f'{attr} = {v}{os.linesep}') 62 | with open(path, 'w') as f: 63 | f.writelines(lines) 64 | 65 | 66 | def load_config(config_path=None, myconfig="myconfig.py"): 67 | 68 | if config_path is None: 69 | main_path = os.getcwd() 70 | config_path = os.path.join(main_path, 'config.py') 71 | if not os.path.exists(config_path): 72 | local_config = os.path.join(os.path.curdir, 'config.py') 73 | if os.path.exists(local_config): 74 | config_path = local_config 75 | 76 | logger.info(f'loading config file: {config_path}') 77 | cfg = Config() 78 | cfg.from_pyfile(config_path) 79 | 80 | # look for the optional myconfig.py in the same path. 81 | personal_cfg_path = config_path.replace("config.py", myconfig) 82 | if os.path.exists(personal_cfg_path): 83 | logger.info(f"loading personal config over-rides from {myconfig}") 84 | personal_cfg = Config() 85 | personal_cfg.from_pyfile(personal_cfg_path) 86 | cfg.from_object(personal_cfg) 87 | else: 88 | logger.warning(f"personal config: file not found {personal_cfg_path}") 89 | 90 | return cfg 91 | -------------------------------------------------------------------------------- /donkeycar/parts/teensy.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import donkeycar as dk 3 | import re 4 | import time 5 | 6 | class TeensyRCin: 7 | def __init__(self): 8 | self.inSteering = 0.0 9 | self.inThrottle = 0.0 10 | 11 | self.sensor = dk.parts.actuator.Teensy(0) 12 | 13 | TeensyRCin.LEFT_ANGLE = -1.0 14 | TeensyRCin.RIGHT_ANGLE = 1.0 15 | TeensyRCin.MIN_THROTTLE = -1.0 16 | TeensyRCin.MAX_THROTTLE = 1.0 17 | 18 | TeensyRCin.LEFT_PULSE = 496.0 19 | TeensyRCin.RIGHT_PULSE = 242.0 20 | TeensyRCin.MAX_PULSE = 496.0 21 | TeensyRCin.MIN_PULSE = 242.0 22 | 23 | 24 | self.on = True 25 | 26 | def map_range(self, x, X_min, X_max, Y_min, Y_max): 27 | ''' 28 | Linear mapping between two ranges of values 29 | ''' 30 | X_range = X_max - X_min 31 | Y_range = Y_max - Y_min 32 | XY_ratio = X_range/Y_range 33 | 34 | return ((x-X_min) / XY_ratio + Y_min) 35 | 36 | def update(self): 37 | rcin_pattern = re.compile('^I +([.0-9]+) +([.0-9]+).*$') 38 | 39 | while self.on: 40 | start = datetime.now() 41 | 42 | l = self.sensor.teensy_readline() 43 | 44 | while l: 45 | # print("mw TeensyRCin line= " + l.decode('utf-8')) 46 | m = rcin_pattern.match(l.decode('utf-8')) 47 | 48 | if m: 49 | i = float(m.group(1)) 50 | if i == 0.0: 51 | self.inSteering = 0.0 52 | else: 53 | i = i / (1000.0 * 1000.0) # in seconds 54 | i *= self.sensor.frequency * 4096.0 55 | self.inSteering = self.map_range(i, 56 | TeensyRCin.LEFT_PULSE, TeensyRCin.RIGHT_PULSE, 57 | TeensyRCin.LEFT_ANGLE, TeensyRCin.RIGHT_ANGLE) 58 | 59 | k = float(m.group(2)) 60 | if k == 0.0: 61 | self.inThrottle = 0.0 62 | else: 63 | k = k / (1000.0 * 1000.0) # in seconds 64 | k *= self.sensor.frequency * 4096.0 65 | self.inThrottle = self.map_range(k, 66 | TeensyRCin.MIN_PULSE, TeensyRCin.MAX_PULSE, 67 | TeensyRCin.MIN_THROTTLE, TeensyRCin.MAX_THROTTLE) 68 | 69 | # print("matched %.1f %.1f %.1f %.1f" % (i, self.inSteering, k, self.inThrottle)) 70 | l = self.sensor.teensy_readline() 71 | 72 | stop = datetime.now() 73 | s = 0.01 - (stop - start).total_seconds() 74 | if s > 0: 75 | time.sleep(s) 76 | 77 | def run_threaded(self): 78 | return self.inSteering, self.inThrottle 79 | 80 | def shutdown(self): 81 | # indicate that the thread should be stopped 82 | self.on = False 83 | print('stopping TeensyRCin') 84 | time.sleep(.5) 85 | 86 | -------------------------------------------------------------------------------- /donkeycar/parts/coral.py: -------------------------------------------------------------------------------- 1 | """Inference Engine used for inference tasks.""" 2 | 3 | from edgetpu.basic.basic_engine import BasicEngine 4 | import numpy 5 | from PIL import Image 6 | 7 | 8 | class InferenceEngine(BasicEngine): 9 | """Engine used for inference task.""" 10 | 11 | def __init__(self, model_path, device_path=None): 12 | """Creates a BasicEngine with given model. 13 | 14 | Args: 15 | model_path: String, path to TF-Lite Flatbuffer file. 16 | device_path: String, if specified, bind engine with Edge TPU at device_path. 17 | 18 | Raises: 19 | ValueError: An error occurred when the output format of model is invalid. 20 | """ 21 | if device_path: 22 | super().__init__(model_path, device_path) 23 | else: 24 | super().__init__(model_path) 25 | output_tensors_sizes = self.get_all_output_tensors_sizes() 26 | if output_tensors_sizes.size > 2: 27 | raise ValueError( 28 | ('Inference model should have 2 output tensors or less!' 29 | 'This model has {}.'.format(output_tensors_sizes.size))) 30 | 31 | def Inference(self, img): 32 | """Inference image with np array image object. 33 | 34 | This interface assumes the loaded model is trained for image 35 | classification. 36 | 37 | Args: 38 | img: numpy.array image object. 39 | 40 | Returns: 41 | List of (float) which represents inference results. 42 | 43 | Raises: 44 | RuntimeError: when tensor not a single 3 channel image. 45 | Asserts: when image incorrect size. 46 | """ 47 | input_tensor_shape = self.get_input_tensor_shape() 48 | if (input_tensor_shape.size != 4 or input_tensor_shape[3] != 3 or 49 | input_tensor_shape[0] != 1): 50 | raise RuntimeError( 51 | 'Invalid input tensor shape! Expected: [1, height, width, 3]') 52 | _, height, width, _ = input_tensor_shape 53 | assert(height == img.shape[0]) 54 | assert(width == img.shape[1]) 55 | input_tensor = img.flatten() 56 | return self.RunInferenceWithInputTensor(input_tensor) 57 | 58 | def RunInferenceWithInputTensor(self, input_tensor): 59 | """Run inference with raw input tensor. 60 | 61 | This interface requires user to process input data themselves and convert 62 | it to formatted input tensor. 63 | 64 | Args: 65 | input_tensor: numpy.array represents the input tensor. 66 | 67 | Returns: 68 | List of (float) which represents inference. 69 | 70 | Raises: 71 | ValueError: when input param is invalid. 72 | """ 73 | _, self._raw_result = self.RunInference( 74 | input_tensor) 75 | return [self._raw_result] 76 | 77 | 78 | 79 | class CoralLinearPilot(object): 80 | ''' 81 | Base class for TFlite models that will provide steering and throttle to guide a car. 82 | ''' 83 | def __init__(self): 84 | self.model = None 85 | self.engine = None 86 | 87 | def load(self, model_path): 88 | # Load Coral edgetpu TFLite model and allocate tensors. 89 | self.engine = InferenceEngine(model_path) 90 | 91 | def run(self, image): 92 | steering, throttle = self.engine.Inference(image)[0] 93 | return steering, throttle 94 | -------------------------------------------------------------------------------- /donkeycar/tests/test_seekable_v2.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | import unittest 4 | 5 | from donkeycar.parts.datastore_v2 import Seekable 6 | 7 | 8 | class TestSeekeable(unittest.TestCase): 9 | 10 | def setUp(self): 11 | self._file, self._path = tempfile.mkstemp() 12 | 13 | def test_offset_tracking(self): 14 | appendable = Seekable(self._path) 15 | with appendable: 16 | appendable.writeline('Line 1') 17 | appendable.writeline('Line 2') 18 | self.assertEqual(len(appendable.line_lengths), 2) 19 | appendable.seek_line_start(1) 20 | self.assertEqual(appendable.readline(), 'Line 1') 21 | appendable.seek_line_start(2) 22 | self.assertEqual(appendable.readline(), 'Line 2') 23 | appendable.seek_end_of_file() 24 | appendable.truncate_until_end(1) 25 | appendable.writeline('Line 2 Revised') 26 | appendable.seek_line_start(2) 27 | self.assertEqual(appendable.readline(), 'Line 2 Revised') 28 | 29 | def test_read_from_and_update(self): 30 | appendable = Seekable(self._path) 31 | with appendable: 32 | appendable.writeline('Line 1') 33 | appendable.writeline('Line 2') 34 | appendable.writeline('Line 3') 35 | # Test idempotent read 36 | current_offset = appendable.file.tell() 37 | lines = appendable.read_from(2) 38 | self.assertEqual(len(lines), 2) 39 | self.assertEqual(lines[0], 'Line 2') 40 | self.assertEqual(lines[1], 'Line 3') 41 | self.assertEqual(appendable.file.tell(), current_offset) 42 | # Test update 43 | appendable.update_line(1, 'Line 1 is longer') 44 | lines = appendable.read_from(1) 45 | self.assertEqual(len(lines), 3) 46 | self.assertEqual(lines[0], 'Line 1 is longer') 47 | self.assertEqual(lines[1], 'Line 2') 48 | self.assertEqual(lines[2], 'Line 3') 49 | 50 | def test_read_contents(self): 51 | appendable = Seekable(self._path) 52 | with appendable: 53 | appendable.writeline('Line 1') 54 | appendable.writeline('Line 2') 55 | self.assertEqual(len(appendable.line_lengths), 2) 56 | appendable.file.seek(0) 57 | appendable._read_contents() 58 | self.assertEqual(len(appendable.line_lengths), 2) 59 | 60 | def test_restore_from_index(self): 61 | appendable = Seekable(self._path) 62 | with appendable: 63 | appendable.writeline('Line 1') 64 | appendable.writeline('Line 2') 65 | self.assertEqual(len(appendable.line_lengths), 2) 66 | 67 | appendable = Seekable(self._path, line_lengths=appendable.line_lengths) 68 | with appendable: 69 | self.assertEqual(len(appendable.line_lengths), 2) 70 | appendable.seek_line_start(1) 71 | self.assertEqual(appendable.readline(), 'Line 1') 72 | self.assertEqual(appendable.readline(), 'Line 2') 73 | 74 | def tearDown(self): 75 | os.remove(self._path) 76 | 77 | 78 | if __name__ == '__main__': 79 | unittest.main() 80 | -------------------------------------------------------------------------------- /scripts/convert_to_tub_v2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | ''' 3 | Usage: 4 | convert_to_tub_v2.py --tub= --output= 5 | 6 | Note: 7 | This script converts the old datastore format to the new datastore format. 8 | ''' 9 | 10 | import json 11 | import os 12 | import traceback 13 | from pathlib import Path 14 | 15 | from docopt import docopt 16 | from PIL import Image 17 | from progress.bar import IncrementalBar 18 | 19 | from donkeycar.parts.datastore import Tub as LegacyTub 20 | from donkeycar.parts.tub_v2 import Tub 21 | 22 | 23 | def convert_to_tub_v2(paths, output_path): 24 | """ 25 | Convert from old tubs to new one 26 | 27 | :param paths: legacy tub paths 28 | :param output_path: new tub output path 29 | :return: None 30 | """ 31 | empty_record = {'__empty__': True} 32 | if type(paths) is str: 33 | paths = [paths] 34 | legacy_tubs = [LegacyTub(path) for path in paths] 35 | print(f'Total number of tubs: {len(legacy_tubs)}') 36 | 37 | for legacy_tub in legacy_tubs: 38 | # add input and type for empty records recording 39 | inputs = legacy_tub.inputs + ['__empty__'] 40 | types = legacy_tub.types + ['boolean'] 41 | output_tub = Tub(output_path, inputs, types, 42 | list(legacy_tub.meta.items())) 43 | record_paths = legacy_tub.gather_records() 44 | bar = IncrementalBar('Converting', max=len(record_paths)) 45 | previous_index = None 46 | for record_path in record_paths: 47 | try: 48 | contents = Path(record_path).read_text() 49 | record = json.loads(contents) 50 | image_path = record['cam/image_array'] 51 | current_index = int(image_path.split('_')[0]) 52 | image_path = os.path.join(legacy_tub.path, image_path) 53 | image_data = Image.open(image_path) 54 | record['cam/image_array'] = image_data 55 | # first record or they are continuous, just append 56 | if not previous_index or current_index == previous_index + 1: 57 | output_tub.write_record(record) 58 | previous_index = current_index 59 | # otherwise fill the gap with empty records 60 | else: 61 | # Skipping over previous record here because it has 62 | # already been written. 63 | previous_index += 1 64 | # Adding empty record nodes, and marking them deleted 65 | # until the next valid record. 66 | delete_list = [] 67 | while previous_index < current_index: 68 | idx = output_tub.manifest.current_index 69 | output_tub.write_record(empty_record) 70 | delete_list.append(idx) 71 | previous_index += 1 72 | output_tub.delete_records(delete_list) 73 | bar.next() 74 | except Exception as exception: 75 | print(f'Ignoring record path {record_path}\n', exception) 76 | traceback.print_exc() 77 | # writing session id into manifest metadata 78 | output_tub.close() 79 | 80 | 81 | if __name__ == '__main__': 82 | args = docopt(__doc__) 83 | 84 | input_path = args["--tub"] 85 | output_path = args["--output"] 86 | paths = input_path.split(',') 87 | convert_to_tub_v2(paths, output_path) 88 | -------------------------------------------------------------------------------- /donkeycar/parts/image.py: -------------------------------------------------------------------------------- 1 | 2 | from PIL import Image 3 | import numpy as np 4 | from donkeycar.utils import img_to_binary, binary_to_img, arr_to_img, \ 5 | img_to_arr, normalize_image 6 | 7 | 8 | class ImgArrToJpg(): 9 | 10 | def run(self, img_arr): 11 | if img_arr is None: 12 | return None 13 | try: 14 | image = arr_to_img(img_arr) 15 | jpg = img_to_binary(image) 16 | return jpg 17 | except: 18 | return None 19 | 20 | 21 | class JpgToImgArr(): 22 | 23 | def run(self, jpg): 24 | if jpg is None: 25 | return None 26 | image = binary_to_img(jpg) 27 | img_arr = img_to_arr(image) 28 | return img_arr 29 | 30 | 31 | class StereoPair: 32 | ''' 33 | take two images and put together in a single image 34 | ''' 35 | def run(self, image_a, image_b): 36 | ''' 37 | This will take the two images and combine them into a single image 38 | One in red, the other in green, and diff in blue channel. 39 | ''' 40 | if image_a is not None and image_b is not None: 41 | width, height, _ = image_a.shape 42 | grey_a = dk.utils.rgb2gray(image_a) 43 | grey_b = dk.utils.rgb2gray(image_b) 44 | grey_c = grey_a - grey_b 45 | 46 | stereo_image = np.zeros([width, height, 3], dtype=np.dtype('B')) 47 | stereo_image[...,0] = np.reshape(grey_a, (width, height)) 48 | stereo_image[...,1] = np.reshape(grey_b, (width, height)) 49 | stereo_image[...,2] = np.reshape(grey_c, (width, height)) 50 | else: 51 | stereo_image = [] 52 | 53 | return np.array(stereo_image) 54 | 55 | 56 | class ImgCrop: 57 | """ 58 | Crop an image to an area of interest. 59 | """ 60 | def __init__(self, top=0, bottom=0, left=0, right=0): 61 | self.top = top 62 | self.bottom = bottom 63 | self.left = left 64 | self.right = right 65 | 66 | def run(self, img_arr): 67 | if img_arr is None: 68 | return None 69 | width, height, _ = img_arr.shape 70 | img_arr = img_arr[self.top:height-self.bottom, 71 | self.left: width-self.right] 72 | return img_arr 73 | 74 | def shutdown(self): 75 | pass 76 | 77 | 78 | class ImgStack: 79 | """ 80 | Stack N previous images into a single N channel image, after converting 81 | each to grayscale. The most recent image is the last channel, and pushes 82 | previous images towards the front. 83 | """ 84 | def __init__(self, num_channels=3): 85 | self.img_arr = None 86 | self.num_channels = num_channels 87 | 88 | def rgb2gray(self, rgb): 89 | ''' 90 | take a numpy rgb image return a new single channel image converted to 91 | greyscale 92 | ''' 93 | return np.dot(rgb[...,:3], [0.299, 0.587, 0.114]) 94 | 95 | def run(self, img_arr): 96 | width, height, _ = img_arr.shape 97 | gray = self.rgb2gray(img_arr) 98 | 99 | if self.img_arr is None: 100 | self.img_arr = np.zeros([width, height, self.num_channels], dtype=np.dtype('B')) 101 | 102 | for ch in range(self.num_channels - 1): 103 | self.img_arr[...,ch] = self.img_arr[...,ch+1] 104 | 105 | self.img_arr[...,self.num_channels - 1:] = np.reshape(gray, (width, height, 1)) 106 | 107 | return self.img_arr 108 | 109 | def shutdown(self): 110 | pass 111 | -------------------------------------------------------------------------------- /donkeycar/management/ui/rc_file_handler.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | from datetime import datetime 3 | import os 4 | from collections import namedtuple 5 | 6 | import yaml 7 | from kivy import Logger 8 | 9 | # Data struct to show tub field in the progress bar, containing the name, 10 | # the name of the maximum value in the config file and if it is centered. 11 | FieldProperty = namedtuple('FieldProperty', 12 | ['field', 'max_value_id', 'centered']) 13 | 14 | 15 | def recursive_update(target, source): 16 | """ Recursively update dictionary """ 17 | if isinstance(target, dict) and isinstance(source, dict): 18 | for k, v in source.items(): 19 | v_t = target.get(k) 20 | if not recursive_update(v_t, v): 21 | target[k] = v 22 | return True 23 | else: 24 | return False 25 | 26 | 27 | class RcFileHandler: 28 | """ This handles the config file which stores the data, like the field 29 | mapping for displaying of bars and last opened car, tub directory. """ 30 | 31 | # These entries are expected in every tub, so they don't need to be in 32 | # the file 33 | known_entries = [ 34 | FieldProperty('user/angle', '', centered=True), 35 | FieldProperty('user/throttle', '', centered=False), 36 | FieldProperty('pilot/angle', '', centered=True), 37 | FieldProperty('pilot/throttle', '', centered=False), 38 | ] 39 | 40 | def __init__(self, file_path='~/.donkeyrc'): 41 | self.file_path = os.path.expanduser(file_path) 42 | self.data = self.create_data() 43 | recursive_update(self.data, self.read_file()) 44 | self.field_properties = self.create_field_properties() 45 | 46 | def exit_hook(): 47 | self.write_file() 48 | # Automatically save config when program ends 49 | atexit.register(exit_hook) 50 | 51 | def create_field_properties(self): 52 | """ Merges known field properties with the ones from the file """ 53 | field_properties = {entry.field: entry for entry in self.known_entries} 54 | field_list = self.data.get('field_mapping') 55 | if field_list is None: 56 | field_list = {} 57 | for entry in field_list: 58 | assert isinstance(entry, dict), \ 59 | 'Dictionary required in each entry in the field_mapping list' 60 | field_property = FieldProperty(**entry) 61 | field_properties[field_property.field] = field_property 62 | return field_properties 63 | 64 | def create_data(self): 65 | data = dict() 66 | data['user_pilot_map'] = {'user/throttle': 'pilot/throttle', 67 | 'user/angle': 'pilot/angle'} 68 | data['pilots'] = [] 69 | data['config_params'] = [] 70 | return data 71 | 72 | def read_file(self): 73 | if os.path.exists(self.file_path): 74 | with open(self.file_path) as f: 75 | data = yaml.load(f, Loader=yaml.FullLoader) 76 | Logger.info(f'Donkeyrc: Donkey file {self.file_path} loaded.') 77 | return data 78 | else: 79 | Logger.warn(f'Donkeyrc: Donkey file {self.file_path} does not ' 80 | f'exist.') 81 | return {} 82 | 83 | def write_file(self): 84 | if os.path.exists(self.file_path): 85 | Logger.info(f'Donkeyrc: Donkey file {self.file_path} updated.') 86 | with open(self.file_path, mode='w') as f: 87 | self.data['time_stamp'] = datetime.now() 88 | data = yaml.dump(self.data, f) 89 | return data 90 | 91 | 92 | rc_handler = RcFileHandler() 93 | -------------------------------------------------------------------------------- /donkeycar/parts/pytorch/ResNet18.py: -------------------------------------------------------------------------------- 1 | import pytorch_lightning as pl 2 | import torchvision.models as models 3 | import torchmetrics 4 | import torch.nn as nn 5 | import torch.nn.functional as F 6 | import torch 7 | import numpy as np 8 | import torch.optim as optim 9 | from donkeycar.parts.pytorch.torch_data import get_default_transform 10 | 11 | 12 | def load_resnet18(num_classes=2): 13 | # Load the pre-trained model (on ImageNet) 14 | model = models.resnet18(pretrained=True) 15 | 16 | # Don't allow model feature extraction layers to be modified 17 | for layer in model.parameters(): 18 | layer.requires_grad = False 19 | 20 | # Change the classifier layer 21 | model.fc = nn.Linear(512, 2) 22 | 23 | for param in model.fc.parameters(): 24 | param.requires_grad = True 25 | 26 | return model 27 | 28 | 29 | class ResNet18(pl.LightningModule): 30 | def __init__(self, input_shape=(128, 3, 224, 224), output_size=(2,)): 31 | super().__init__() 32 | 33 | # Used by PyTorch Lightning to print an example model summary 34 | self.example_input_array = torch.rand(input_shape) 35 | 36 | # Metrics 37 | self.train_mse = torchmetrics.MeanSquaredError() 38 | self.valid_mse = torchmetrics.MeanSquaredError() 39 | 40 | self.model = load_resnet18(num_classes=output_size[0]) 41 | 42 | self.inference_transform = get_default_transform(for_inference=True) 43 | 44 | # Keep track of the loss history. This is useful for writing tests 45 | self.loss_history = [] 46 | 47 | def forward(self, x): 48 | # Forward defines the prediction/inference action 49 | return self.model(x) 50 | 51 | def training_step(self, batch, batch_idx): 52 | x, y = batch 53 | logits = self.model(x) 54 | 55 | loss = F.l1_loss(logits, y) 56 | self.loss_history.append(loss) 57 | self.log("train_loss", loss) 58 | 59 | # Log Metrics 60 | self.train_mse(logits, y) 61 | self.log("train_mse", self.train_mse, on_step=False, on_epoch=True) 62 | 63 | return loss 64 | 65 | def validation_step(self, batch, batch_idx): 66 | x, y = batch 67 | logits = self.forward(x) 68 | loss = F.l1_loss(logits, y) 69 | 70 | self.log("val_loss", loss) 71 | 72 | # Log Metrics 73 | self.valid_mse(logits, y) 74 | self.log("valid_mse", self.valid_mse, on_step=False, on_epoch=True) 75 | 76 | def configure_optimizers(self): 77 | optimizer = optim.Adam(self.model.parameters(), lr=0.0001, weight_decay=0.0005) 78 | return optimizer 79 | 80 | def run(self, img_arr: np.ndarray, other_arr: np.ndarray = None): 81 | """ 82 | Donkeycar parts interface to run the part in the loop. 83 | 84 | :param img_arr: uint8 [0,255] numpy array with image data 85 | :param other_arr: numpy array of additional data to be used in the 86 | pilot, like IMU array for the IMU model or a 87 | state vector in the Behavioural model 88 | :return: tuple of (angle, throttle) 89 | """ 90 | from PIL import Image 91 | 92 | pil_image = Image.fromarray(img_arr) 93 | tensor_image = self.inference_transform(pil_image) 94 | tensor_image = tensor_image.unsqueeze(0) 95 | 96 | # Result is (1, 2) 97 | result = self.forward(tensor_image) 98 | 99 | # Resize to (2,) 100 | result = result.reshape(-1) 101 | 102 | # Convert from being normalized between [0, 1] to being between [-1, 1] 103 | result = result * 2 - 1 104 | print("ResNet18 result: {}".format(result)) 105 | return result 106 | -------------------------------------------------------------------------------- /donkeycar/parts/dgym.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import gym 4 | import gym_donkeycar 5 | 6 | 7 | def is_exe(fpath): 8 | return os.path.isfile(fpath) and os.access(fpath, os.X_OK) 9 | 10 | 11 | class DonkeyGymEnv(object): 12 | 13 | def __init__(self, sim_path, host="127.0.0.1", port=9091, headless=0, env_name="donkey-generated-track-v0", sync="asynchronous", conf={}, record_location=False, record_gyroaccel=False, record_velocity=False, record_lidar=False, delay=0): 14 | 15 | if sim_path != "remote": 16 | if not os.path.exists(sim_path): 17 | raise Exception( 18 | "The path you provided for the sim does not exist.") 19 | 20 | if not is_exe(sim_path): 21 | raise Exception("The path you provided is not an executable.") 22 | 23 | conf["exe_path"] = sim_path 24 | conf["host"] = host 25 | conf["port"] = port 26 | conf["guid"] = 0 27 | conf["frame_skip"] = 1 28 | self.env = gym.make(env_name, conf=conf) 29 | self.frame = self.env.reset() 30 | self.action = [0.0, 0.0, 0.0] 31 | self.running = True 32 | self.info = {'pos': (0., 0., 0.), 33 | 'speed': 0, 34 | 'cte': 0, 35 | 'gyro': (0., 0., 0.), 36 | 'accel': (0., 0., 0.), 37 | 'vel': (0., 0., 0.), 38 | 'lidar': []} 39 | self.delay = float(delay) / 1000 40 | self.record_location = record_location 41 | self.record_gyroaccel = record_gyroaccel 42 | self.record_velocity = record_velocity 43 | self.record_lidar = record_lidar 44 | 45 | self.buffer = [] 46 | 47 | def delay_buffer(self, frame, info): 48 | now = time.time() 49 | buffer_tuple = (now, frame, info) 50 | self.buffer.append(buffer_tuple) 51 | 52 | # go through the buffer 53 | num_to_remove = 0 54 | for buf in self.buffer: 55 | if now - buf[0] >= self.delay: 56 | num_to_remove += 1 57 | self.frame = buf[1] 58 | else: 59 | break 60 | 61 | # clear the buffer 62 | del self.buffer[:num_to_remove] 63 | 64 | def update(self): 65 | while self.running: 66 | if self.delay > 0.0: 67 | current_frame, _, _, current_info = self.env.step(self.action) 68 | self.delay_buffer(current_frame, current_info) 69 | else: 70 | self.frame, _, _, self.info = self.env.step(self.action) 71 | 72 | def run_threaded(self, steering, throttle, brake=None): 73 | if steering is None or throttle is None: 74 | steering = 0.0 75 | throttle = 0.0 76 | if brake is None: 77 | brake = 0.0 78 | 79 | self.action = [steering, throttle, brake] 80 | 81 | # Output Sim-car position information if configured 82 | outputs = [self.frame] 83 | if self.record_location: 84 | outputs += self.info['pos'][0], self.info['pos'][1], self.info['pos'][2], self.info['speed'], self.info['cte'] 85 | if self.record_gyroaccel: 86 | outputs += self.info['gyro'][0], self.info['gyro'][1], self.info['gyro'][2], self.info['accel'][0], self.info['accel'][1], self.info['accel'][2] 87 | if self.record_velocity: 88 | outputs += self.info['vel'][0], self.info['vel'][1], self.info['vel'][2] 89 | if self.record_lidar: 90 | outputs += [self.info['lidar']] 91 | if len(outputs) == 1: 92 | return self.frame 93 | else: 94 | return outputs 95 | 96 | def shutdown(self): 97 | self.running = False 98 | time.sleep(0.2) 99 | self.env.close() 100 | -------------------------------------------------------------------------------- /donkeycar/tests/test_telemetry.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import time 4 | from unittest import mock 5 | from unittest.mock import patch, MagicMock 6 | import donkeycar.templates.cfg_complete as cfg 7 | from donkeycar.parts.telemetry import MqttTelemetry 8 | import pytest 9 | from random import randint 10 | 11 | 12 | @patch('donkeycar.parts.telemetry.MQTTClient') 13 | def test_mqtt_telemetry(mock_mqtt_client): 14 | """Test MQTT telemetry functionality with mocked MQTT client""" 15 | 16 | # Setup configuration 17 | cfg.TELEMETRY_DEFAULT_INPUTS = 'pilot/angle,pilot/throttle' 18 | cfg.TELEMETRY_DONKEY_NAME = 'test{}'.format(randint(0, 1000)) 19 | cfg.TELEMETRY_MQTT_JSON_ENABLE = True 20 | 21 | # Create mock MQTT client 22 | mock_client_instance = MagicMock() 23 | mock_mqtt_client.return_value = mock_client_instance 24 | 25 | # Create telemetry instance 26 | t = MqttTelemetry(cfg) 27 | 28 | # Verify MQTT client was initialized correctly 29 | mock_mqtt_client.assert_called_once_with(callback_api_version=mock.ANY) 30 | mock_client_instance.connect.assert_called_once_with( 31 | cfg.TELEMETRY_MQTT_BROKER_HOST, 32 | cfg.TELEMETRY_MQTT_BROKER_PORT 33 | ) 34 | mock_client_instance.loop_start.assert_called_once() 35 | 36 | # Test adding step inputs 37 | t.add_step_inputs(inputs=['my/voltage'], types=['float']) 38 | expected_inputs = ['pilot/angle', 'pilot/throttle', 'my/voltage'] 39 | expected_types = ['float', 'float', 'float'] 40 | assert t._step_inputs == expected_inputs 41 | assert t._step_types == expected_types 42 | 43 | # Test initial publish (should do nothing as queue is empty) 44 | t.publish() 45 | mock_client_instance.publish.assert_not_called() 46 | 47 | # Test reporting data 48 | timestamp = t.report({'my/speed': 16, 'my/voltage': 12}) 49 | assert isinstance(timestamp, int) 50 | assert t.qsize == 1 51 | 52 | # Test run method (adds step inputs to queue) 53 | t.run(33.3, 22.2, 11.1) 54 | assert t.qsize == 2 55 | 56 | # Test publishing with data 57 | t.publish() 58 | assert t.qsize == 0 59 | 60 | # Verify publish was called 61 | assert mock_client_instance.publish.called 62 | call_args = mock_client_instance.publish.call_args 63 | topic, payload = call_args[0] 64 | 65 | # Verify topic format 66 | expected_topic = cfg.TELEMETRY_MQTT_TOPIC_TEMPLATE % cfg.TELEMETRY_DONKEY_NAME 67 | assert topic == expected_topic 68 | 69 | # Verify JSON payload structure 70 | import json 71 | payload_data = json.loads(payload) 72 | assert isinstance(payload_data, list) 73 | assert len(payload_data) >= 1 74 | 75 | # Check that data contains expected keys 76 | data_entry = payload_data[0] 77 | assert 'ts' in data_entry 78 | assert 'values' in data_entry 79 | assert 'my/speed' in data_entry['values'] 80 | assert 'pilot/angle' in data_entry['values'] 81 | assert 'pilot/throttle' in data_entry['values'] 82 | 83 | 84 | def test_mqtt_telemetry_connection_error(): 85 | """Test MQTT telemetry handles connection errors gracefully""" 86 | 87 | cfg.TELEMETRY_DEFAULT_INPUTS = 'pilot/angle,pilot/throttle' 88 | cfg.TELEMETRY_DONKEY_NAME = 'test{}'.format(randint(0, 1000)) 89 | cfg.TELEMETRY_MQTT_JSON_ENABLE = True 90 | 91 | with patch('donkeycar.parts.telemetry.MQTTClient') as mock_mqtt_client: 92 | # Simulate connection failure 93 | mock_client_instance = MagicMock() 94 | mock_client_instance.connect.side_effect = ConnectionError("Connection failed") 95 | mock_mqtt_client.return_value = mock_client_instance 96 | 97 | # Connection error should be raised during initialization 98 | with pytest.raises(ConnectionError): 99 | t = MqttTelemetry(cfg) 100 | -------------------------------------------------------------------------------- /donkeycar/parts/imu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import time 3 | SENSOR_MPU6050 = 'mpu6050' 4 | SENSOR_MPU9250 = 'mpu9250' 5 | 6 | DLP_SETTING_DISABLED = 0 7 | CONFIG_REGISTER = 0x1A 8 | 9 | class IMU: 10 | ''' 11 | Installation: 12 | 13 | - MPU6050 14 | sudo apt install python3-smbus 15 | or 16 | sudo apt-get install i2c-tools libi2c-dev python-dev python3-dev 17 | git clone https://github.com/pimoroni/py-smbus.git 18 | cd py-smbus/library 19 | python setup.py build 20 | sudo python setup.py install 21 | 22 | pip install mpu6050-raspberrypi 23 | 24 | - MPU9250 25 | pip install mpu9250-jmdev 26 | 27 | ''' 28 | 29 | def __init__(self, addr=0x68, poll_delay=0.0166, sensor=SENSOR_MPU6050, dlp_setting=DLP_SETTING_DISABLED): 30 | self.sensortype = sensor 31 | if self.sensortype == SENSOR_MPU6050: 32 | from mpu6050 import mpu6050 as MPU6050 33 | self.sensor = MPU6050(addr) 34 | 35 | if(dlp_setting > 0): 36 | self.sensor.bus.write_byte_data(self.sensor.address, CONFIG_REGISTER, dlp_setting) 37 | 38 | else: 39 | from mpu9250_jmdev.registers import AK8963_ADDRESS, GFS_1000, AFS_4G, AK8963_BIT_16, AK8963_MODE_C100HZ 40 | from mpu9250_jmdev.mpu_9250 import MPU9250 41 | 42 | self.sensor = MPU9250( 43 | address_ak=AK8963_ADDRESS, 44 | address_mpu_master=addr, # In 0x68 Address 45 | address_mpu_slave=None, 46 | bus=1, 47 | gfs=GFS_1000, 48 | afs=AFS_4G, 49 | mfs=AK8963_BIT_16, 50 | mode=AK8963_MODE_C100HZ) 51 | 52 | if(dlp_setting > 0): 53 | self.sensor.writeSlave(CONFIG_REGISTER, dlp_setting) 54 | self.sensor.calibrateMPU6500() 55 | self.sensor.configure() 56 | 57 | 58 | self.accel = { 'x' : 0., 'y' : 0., 'z' : 0. } 59 | self.gyro = { 'x' : 0., 'y' : 0., 'z' : 0. } 60 | self.mag = {'x': 0., 'y': 0., 'z': 0.} 61 | self.temp = 0. 62 | self.poll_delay = poll_delay 63 | self.on = True 64 | 65 | def update(self): 66 | while self.on: 67 | self.poll() 68 | time.sleep(self.poll_delay) 69 | 70 | def poll(self): 71 | try: 72 | if self.sensortype == SENSOR_MPU6050: 73 | self.accel, self.gyro, self.temp = self.sensor.get_all_data() 74 | else: 75 | from mpu9250_jmdev.registers import GRAVITY 76 | ret = self.sensor.getAllData() 77 | self.accel = { 'x' : ret[1] * GRAVITY, 'y' : ret[2] * GRAVITY, 'z' : ret[3] * GRAVITY } 78 | self.gyro = { 'x' : ret[4], 'y' : ret[5], 'z' : ret[6] } 79 | self.mag = { 'x' : ret[13], 'y' : ret[14], 'z' : ret[15] } 80 | self.temp = ret[16] 81 | except: 82 | print('failed to read imu!!') 83 | 84 | def run_threaded(self): 85 | return self.accel['x'], self.accel['y'], self.accel['z'], self.gyro['x'], self.gyro['y'], self.gyro['z'], self.temp 86 | 87 | def run(self): 88 | self.poll() 89 | return self.accel['x'], self.accel['y'], self.accel['z'], self.gyro['x'], self.gyro['y'], self.gyro['z'], self.temp 90 | 91 | def shutdown(self): 92 | self.on = False 93 | 94 | 95 | if __name__ == "__main__": 96 | iter = 0 97 | import sys 98 | sensor_type = SENSOR_MPU6050 99 | dlp_setting = DLP_SETTING_DISABLED 100 | if len(sys.argv) > 1: 101 | sensor_type = sys.argv[1] 102 | if len(sys.argv) > 2: 103 | dlp_setting = int(sys.argv[2]) 104 | 105 | p = IMU(sensor=sensor_type) 106 | while iter < 100: 107 | data = p.run() 108 | print(data) 109 | time.sleep(0.1) 110 | iter += 1 111 | 112 | -------------------------------------------------------------------------------- /donkeycar/parts/led_status.py: -------------------------------------------------------------------------------- 1 | import time 2 | import RPi.GPIO as GPIO 3 | 4 | class LED: 5 | ''' 6 | Toggle a GPIO pin for led control 7 | ''' 8 | def __init__(self, pin): 9 | self.pin = pin 10 | 11 | GPIO.setmode(GPIO.BOARD) 12 | GPIO.setup(self.pin, GPIO.OUT) 13 | self.blink_changed = 0 14 | self.on = False 15 | 16 | def toggle(self, condition): 17 | if condition: 18 | GPIO.output(self.pin, GPIO.HIGH) 19 | self.on = True 20 | else: 21 | GPIO.output(self.pin, GPIO.LOW) 22 | self.on = False 23 | 24 | def blink(self, rate): 25 | if time.time() - self.blink_changed > rate: 26 | self.toggle(not self.on) 27 | self.blink_changed = time.time() 28 | 29 | def run(self, blink_rate): 30 | if blink_rate == 0: 31 | self.toggle(False) 32 | elif blink_rate > 0: 33 | self.blink(blink_rate) 34 | else: 35 | self.toggle(True) 36 | 37 | def shutdown(self): 38 | self.toggle(False) 39 | GPIO.cleanup() 40 | 41 | 42 | class RGB_LED: 43 | ''' 44 | Toggle a GPIO pin on at max_duty pwm if condition is true, off if condition is false. 45 | Good for LED pwm modulated 46 | ''' 47 | def __init__(self, pin_r, pin_g, pin_b, invert_flag=False): 48 | self.pin_r = pin_r 49 | self.pin_g = pin_g 50 | self.pin_b = pin_b 51 | self.invert = invert_flag 52 | print('setting up gpio in board mode') 53 | GPIO.setwarnings(False) 54 | GPIO.setmode(GPIO.BOARD) 55 | GPIO.setup(self.pin_r, GPIO.OUT) 56 | GPIO.setup(self.pin_g, GPIO.OUT) 57 | GPIO.setup(self.pin_b, GPIO.OUT) 58 | freq = 50 59 | self.pwm_r = GPIO.PWM(self.pin_r, freq) 60 | self.pwm_g = GPIO.PWM(self.pin_g, freq) 61 | self.pwm_b = GPIO.PWM(self.pin_b, freq) 62 | self.pwm_r.start(0) 63 | self.pwm_g.start(0) 64 | self.pwm_b.start(0) 65 | self.zero = 0 66 | if( self.invert ): 67 | self.zero = 100 68 | 69 | self.rgb = (50, self.zero, self.zero) 70 | 71 | self.blink_changed = 0 72 | self.on = False 73 | 74 | def toggle(self, condition): 75 | if condition: 76 | r, g, b = self.rgb 77 | self.set_rgb_duty(r, g, b) 78 | self.on = True 79 | else: 80 | self.set_rgb_duty(self.zero, self.zero, self.zero) 81 | self.on = False 82 | 83 | def blink(self, rate): 84 | if time.time() - self.blink_changed > rate: 85 | self.toggle(not self.on) 86 | self.blink_changed = time.time() 87 | 88 | def run(self, blink_rate): 89 | if blink_rate == 0: 90 | self.toggle(False) 91 | elif blink_rate > 0: 92 | self.blink(blink_rate) 93 | else: 94 | self.toggle(True) 95 | 96 | def set_rgb(self, r, g, b): 97 | r = r if not self.invert else 100-r 98 | g = g if not self.invert else 100-g 99 | b = b if not self.invert else 100-b 100 | self.rgb = (r, g, b) 101 | self.set_rgb_duty(r, g, b) 102 | 103 | def set_rgb_duty(self, r, g, b): 104 | self.pwm_r.ChangeDutyCycle(r) 105 | self.pwm_g.ChangeDutyCycle(g) 106 | self.pwm_b.ChangeDutyCycle(b) 107 | 108 | def shutdown(self): 109 | self.toggle(False) 110 | GPIO.cleanup() 111 | 112 | 113 | if __name__ == "__main__": 114 | import time 115 | import sys 116 | pin_r = int(sys.argv[1]) 117 | pin_g = int(sys.argv[2]) 118 | pin_b = int(sys.argv[3]) 119 | rate = float(sys.argv[4]) 120 | print('output pin', pin_r, pin_g, pin_b, 'rate', rate) 121 | 122 | p = RGB_LED(pin_r, pin_g, pin_b) 123 | 124 | iter = 0 125 | while iter < 50: 126 | p.run(rate) 127 | time.sleep(0.1) 128 | iter += 1 129 | 130 | delay = 0.1 131 | 132 | iter = 0 133 | while iter < 100: 134 | p.set_rgb(iter, 100-iter, 0) 135 | time.sleep(delay) 136 | iter += 1 137 | 138 | iter = 0 139 | while iter < 100: 140 | p.set_rgb(100 - iter, 0, iter) 141 | time.sleep(delay) 142 | iter += 1 143 | 144 | p.shutdown() 145 | 146 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Donkeycar 2 | 3 | Thank you for contributing to the Donkeycar project. Here are some guidelines that can help you be successful in getting your contributions merged into Donkeycar. 4 | 5 | ## What makes for a good Pull Request? 6 | - The code implements a bug fix or optimization/refactor to a preexisting feature or adds a new feature. 7 | - If the code is adding a new feature, it would be advisable to open an Issue in the Donkeycar repo prior to opening the PR. This would allow discussion on how the feature may be best implemented and even if the feature is something that would get accepted into Donkeycar. The new feature should have fairly broad applicability; it should be a feature that would be useful to a lot of users. 8 | - The code should be well writte have some comments in it where it is not obvious what is going on and why. It should also be pep-8 compliant in style. Like with any other code, it should avoid code duplications from itself or other existing code. It should keep data and methods arranged in classes as much as possible and should avoid lengthy, monolithic functions and functions with too many input/output parameters. Monkey-patching is a no-go and pythonic style usually is a good guide. 9 | - The code must work on our currently supported platforms; Raspberry Pi OS and the Jetson Nano for cars and Linux, Mac and WSL on Windows. 10 | - The code should have unit tests. 11 | - The code should work on the version of Python specified in the installation documentation at docs.donkeycar.com 12 | - The PR should include instructions/steps telling how the feature/fix can be tested by a person. In some cases you may want to create a video and link it from Youtube or Vimeo to show the process; for instance if it includes mounting hardware. 13 | - For new features or changes to preexisting feature you should also open a PR in the documentation repo https://github.com/autorope/donkeydocs with updated docs. A human tester will want to refer to that when testing. 14 | 15 | 16 | ## What makes for a Pull Request that is not likely to get accepted? 17 | - It adds a feature that is not useful to a broad audience or is too complex/complicated for the Donkeycar audience. For instance, it adds a driver for a custom piece of hardware or a piece of hardware that is not generally obtainable. In this case we will encourage you to maintain the feature in your own fork on Donkeycar and keep that fork up to date with the main branch. 18 | - It does not have unit tests. 19 | - The PR is opened and it receives comments and/or requests for change, but no response is made by the owner of the PR. In this case we will eventually close the PR as unresponsive. 20 | 21 | 22 | ## Pull Request Process 23 | - Fork the Donkeycar repository into your own github account. See [About Forks](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks) in the github docs. 24 | - Create a branch based off the `main` branch in your repository and make the changes. 25 | - Open a pull request to the Donkeycar main repository. If it is associated with a github issue then reference the issue in the pull request. See [Creating a Pull Request from a Fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) in the github docs. 26 | - If the PR is associated with an Issue then link the issue in the PR description. 27 | - If the PR is associated with a documentation change then link to the associated PR in the documentation repo. 28 | - The maintainers will be automatically notified of the pull request. 29 | - Maintainers will provide comments and/or request for changes in the PR. This could take a little while; we have a small volunteer team that is working on a number of initiatives. You can get more visibility by announcing the PR in the software channel on the Discord. 30 | - The owner of the PR should be checking it for comments and/or request for changes and responding. In particular, if there are requests for changes but you cannot get to them reasonably quickly then add a comment in the PR that helps us understand your timeframe so that we don't close that PR as unresponsive. 31 | - There is a possibility that we choose to not move forward with the PR and it will be closed. You can minimize that chance by discussing the feature or fix in an Issue prior to opening a PR (see above). 32 | - If once all requested changes are made then the PR can be accepted. At this point one of the maintainers will merge the PR and the PR will be closed as completed. Congratulations, you just made the world better. 33 | 34 | -------------------------------------------------------------------------------- /donkeycar/parts/text_writer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | 7 | class TextLogger: 8 | """ 9 | Log data to a text file. 10 | A 'row' ends up as one line of text when transformed by row_to_line(). 11 | The base implementation simply treats is as a text line, but subclasses 12 | an overwrite row_to_line() and line_to_row() to save structured data, 13 | like tuples or arrays as CSV. 14 | """ 15 | def __init__(self, file_path:str, append:bool=False, allow_empty_file:bool=False, allow_empty_line:bool=True): 16 | self.file_path = file_path 17 | self.append = append 18 | self.allow_empty_file = allow_empty_file 19 | self.allow_empty_line = allow_empty_line 20 | self.rows = [] 21 | 22 | def run(self, recording, rows): 23 | if recording and len is not None and len(rows) > 0: 24 | self.rows += rows 25 | return self.rows 26 | 27 | def length(self): 28 | return len(self.rows) 29 | 30 | def is_empty(self): 31 | return 0 == self.length() 32 | 33 | def is_loaded(self): 34 | return not self.is_empty() 35 | 36 | def get(self, row_index:int): 37 | return self.rows[row_index] if (row_index >= 0) and (row_index < self.length()) else None 38 | 39 | def reset(self): 40 | self.rows = [] 41 | return True 42 | 43 | def row_to_line(self, row): 44 | """ 45 | convert a row into a string 46 | """ 47 | if row is not None: 48 | line = str(row) 49 | if self.allow_empty_line or len(line) > 0: 50 | return line 51 | return None 52 | 53 | def line_to_row(self, line:str): 54 | """ 55 | convert a string into a row object 56 | """ 57 | if isinstance(line, str): 58 | line = line.rstrip('\n') 59 | if self.allow_empty_line or len(line) > 0: 60 | return line 61 | return None 62 | 63 | def save(self): 64 | if self.is_loaded() or self.allow_empty_file: 65 | with open(self.file_path, "a" if self.append else "w") as fp: 66 | for row in self.rows: 67 | line = self.row_to_line(row) 68 | if line is not None: 69 | fp.write(self.row_to_line(row)) 70 | fp.write('\n') 71 | return True 72 | return False 73 | 74 | def load(self): 75 | if os.path.exists(self.file_path): 76 | rows = [] 77 | with open(self.file_path, "r") as file: 78 | for line in file: 79 | row = self.line_to_row(line) 80 | if row is not None: 81 | rows.append(row) 82 | if rows or self.allow_empty_file: 83 | self.rows = rows 84 | return True 85 | return False 86 | 87 | 88 | class CsvLogger(TextLogger): 89 | """ 90 | Log iterable to a comma-separated text file. 91 | The separator can be customized. 92 | """ 93 | def __init__(self, file_path:str, append:bool=False, allow_empty_file:bool=False, allow_empty_line:bool=True, separator:str=",", field_count:int=None, trim:bool=True): 94 | super().__init__(file_path, append, allow_empty_file, allow_empty_line) 95 | self.separator = separator 96 | self.field_count = field_count 97 | self.trim = trim 98 | 99 | def row_to_line(self, row): 100 | """ 101 | convert a row into a string 102 | """ 103 | if row is not None: 104 | line = self.separator.join([str(field) for field in row]) 105 | if self.allow_empty_line or len(line) > 0: 106 | return line 107 | return None 108 | 109 | def line_to_row(self, line:str): 110 | """ 111 | convert a string into a row object 112 | """ 113 | row = None 114 | if isinstance(line, str): 115 | row = line.rstrip('\n').split(self.separator) 116 | field_count = len(row) 117 | if self.field_count is None or field_count == self.field_count: 118 | if self.trim: 119 | row = [field.strip() for field in row] 120 | else: 121 | row = None 122 | logger.debug(f"CsvLogger: dropping row with field count = {field_count}") 123 | else: 124 | logging.error("CsvLogger: line_to_row expected string") 125 | return row 126 | -------------------------------------------------------------------------------- /donkeycar/parts/realsense2.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Tawn Kramer 3 | File: realsense2.py 4 | Date: April 14 2019 5 | Notes: Parts to input data from Intel Realsense 2 cameras 6 | ''' 7 | import time 8 | import logging 9 | 10 | import numpy as np 11 | import pyrealsense2 as rs 12 | 13 | class RS_T265(object): 14 | ''' 15 | The Intel Realsense T265 camera is a device which uses an imu, twin fisheye cameras, 16 | and an Movidius chip to do sensor fusion and emit a world space coordinate frame that 17 | is remarkably consistent. 18 | ''' 19 | 20 | def __init__(self, image_output=False, calib_filename=None): 21 | # Using the image_output will grab two image streams from the fisheye cameras but return only one. 22 | # This can be a bit much for USB2, but you can try it. Docs recommend USB3 connection for this. 23 | self.image_output = image_output 24 | 25 | # When we have and encoder, this will be the last vel measured. 26 | self.enc_vel_ms = 0.0 27 | self.wheel_odometer = None 28 | 29 | # Declare RealSense pipeline, encapsulating the actual device and sensors 30 | print("starting T265") 31 | self.pipe = rs.pipeline() 32 | cfg = rs.config() 33 | cfg.enable_stream(rs.stream.pose) 34 | profile = cfg.resolve(self.pipe) 35 | dev = profile.get_device() 36 | tm2 = dev.as_tm2() 37 | 38 | 39 | if self.image_output: 40 | #right now it's required for both streams to be enabled 41 | cfg.enable_stream(rs.stream.fisheye, 1) # Left camera 42 | cfg.enable_stream(rs.stream.fisheye, 2) # Right camera 43 | 44 | if calib_filename is not None: 45 | pose_sensor = tm2.first_pose_sensor() 46 | self.wheel_odometer = pose_sensor.as_wheel_odometer() 47 | 48 | # calibration to list of uint8 49 | f = open(calib_filename) 50 | chars = [] 51 | for line in f: 52 | for c in line: 53 | chars.append(ord(c)) # char to uint8 54 | 55 | # load/configure wheel odometer 56 | print("loading wheel config", calib_filename) 57 | self.wheel_odometer.load_wheel_odometery_config(chars) 58 | 59 | 60 | # Start streaming with requested config 61 | self.pipe.start(cfg) 62 | self.running = True 63 | print("Warning: T265 needs a warmup period of a few seconds before it will emit tracking data.") 64 | 65 | zero_vec = (0.0, 0.0, 0.0) 66 | self.pos = zero_vec 67 | self.vel = zero_vec 68 | self.acc = zero_vec 69 | self.img = None 70 | 71 | def poll(self): 72 | 73 | if self.wheel_odometer: 74 | wo_sensor_id = 0 # indexed from 0, match to order in calibration file 75 | frame_num = 0 # not used 76 | v = rs.vector() 77 | v.x = -1.0 * self.enc_vel_ms # m/s 78 | #v.z = -1.0 * self.enc_vel_ms # m/s 79 | self.wheel_odometer.send_wheel_odometry(wo_sensor_id, frame_num, v) 80 | 81 | try: 82 | frames = self.pipe.wait_for_frames() 83 | except Exception as e: 84 | logging.error(e) 85 | return 86 | 87 | if self.image_output: 88 | #We will just get one image for now. 89 | # Left fisheye camera frame 90 | left = frames.get_fisheye_frame(1) 91 | self.img = np.asanyarray(left.get_data()) 92 | 93 | 94 | # Fetch pose frame 95 | pose = frames.get_pose_frame() 96 | 97 | if pose: 98 | data = pose.get_pose_data() 99 | self.pos = data.translation 100 | self.vel = data.velocity 101 | self.acc = data.acceleration 102 | logging.debug('realsense pos(%f, %f, %f)' % (self.pos.x, self.pos.y, self.pos.z)) 103 | 104 | 105 | def update(self): 106 | while self.running: 107 | self.poll() 108 | 109 | def run_threaded(self, enc_vel_ms): 110 | self.enc_vel_ms = enc_vel_ms 111 | return self.pos, self.vel, self.acc, self.img 112 | 113 | def run(self, enc_vel_ms): 114 | self.enc_vel_ms = enc_vel_ms 115 | self.poll() 116 | return self.run_threaded() 117 | 118 | def shutdown(self): 119 | self.running = False 120 | time.sleep(0.1) 121 | self.pipe.stop() 122 | 123 | 124 | 125 | if __name__ == "__main__": 126 | c = RS_T265() 127 | while True: 128 | pos, vel, acc = c.run() 129 | print(pos) 130 | time.sleep(0.1) 131 | c.shutdown() 132 | -------------------------------------------------------------------------------- /donkeycar/tests/test_circular_buffer.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pytest 3 | 4 | from donkeycar.utilities.circular_buffer import CircularBuffer 5 | 6 | class TestCircularBuffer(unittest.TestCase): 7 | 8 | def test_circular_buffer_queue(self): 9 | """ 10 | enqueue items to head 11 | dequeue items from tail 12 | """ 13 | queue:CircularBuffer = CircularBuffer(3, defaultValue="out-of-range") 14 | self.assertEqual(3, queue.capacity) 15 | self.assertEqual(0, queue.count) 16 | 17 | queue.enqueue(0) 18 | self.assertEqual(1, queue.count) 19 | self.assertEqual(0, queue.head()) 20 | self.assertEqual(0, queue.tail()) 21 | 22 | queue.enqueue(1) 23 | self.assertEqual(2, queue.count) 24 | self.assertEqual(1, queue.head()) 25 | self.assertEqual(0, queue.tail()) 26 | 27 | queue.enqueue(2) 28 | self.assertEqual(3, queue.count) 29 | self.assertEqual(2, queue.head()) 30 | self.assertEqual(0, queue.tail()) 31 | 32 | queue.enqueue(3) 33 | self.assertEqual(3, queue.count) 34 | self.assertEqual(3, queue.head()) 35 | self.assertEqual(1, queue.tail()) 36 | 37 | self.assertEqual(1, queue.dequeue()) 38 | self.assertEqual(2, queue.count) 39 | self.assertEqual(3, queue.head()) 40 | self.assertEqual(2, queue.tail()) 41 | 42 | self.assertEqual(2, queue.dequeue()) 43 | self.assertEqual(1, queue.count) 44 | self.assertEqual(3, queue.head()) 45 | self.assertEqual(3, queue.tail()) 46 | 47 | self.assertEqual(3, queue.dequeue()) 48 | self.assertEqual(0, queue.count) 49 | self.assertEqual("out-of-range", queue.head()) 50 | self.assertEqual("out-of-range", queue.tail()) 51 | 52 | self.assertEqual("out-of-range", queue.dequeue()) 53 | 54 | def test_circular_buffer_stack(self): 55 | """ 56 | push items to head 57 | pop items from head 58 | """ 59 | stack:CircularBuffer = CircularBuffer(2, defaultValue="out-of-range") 60 | self.assertEqual(2, stack.capacity) 61 | self.assertEqual(0, stack.count) 62 | self.assertEqual("out-of-range", stack.pop()) 63 | 64 | stack.push(0) 65 | self.assertEqual(1, stack.count) 66 | self.assertEqual(0, stack.head()) 67 | self.assertEqual(0, stack.tail()) 68 | 69 | stack.push(1) 70 | self.assertEqual(2, stack.count) 71 | self.assertEqual(1, stack.head()) 72 | self.assertEqual(0, stack.tail()) 73 | 74 | # pushing onto a full stack raises exception 75 | try: 76 | stack.push(2) 77 | self.fail("should have thrown exception") 78 | except IndexError: 79 | # nothing should have changed 80 | self.assertEqual(2, stack.count) 81 | self.assertEqual(1, stack.head()) 82 | self.assertEqual(0, stack.tail()) 83 | 84 | self.assertEqual(1, stack.pop()) 85 | self.assertEqual(1, stack.count) 86 | self.assertEqual(0, stack.head()) 87 | self.assertEqual(0, stack.tail()) 88 | 89 | self.assertEqual(0, stack.pop()) 90 | self.assertEqual(0, stack.count) 91 | self.assertEqual("out-of-range", stack.head()) 92 | self.assertEqual("out-of-range", stack.tail()) 93 | 94 | # popping from empty stack returns default 95 | self.assertEqual("out-of-range", stack.pop()) 96 | 97 | def test_circular_buffer_array(self): 98 | """ 99 | append items to tail 100 | set/get items by index 101 | """ 102 | array:CircularBuffer = CircularBuffer(2, defaultValue="out-of-range") 103 | self.assertEqual(2, array.capacity) 104 | self.assertEqual(0, array.count) 105 | self.assertEqual("out-of-range", array.get(0)) 106 | 107 | array.append(0) 108 | self.assertEqual(1, array.count) 109 | self.assertEqual(0, array.head()) 110 | self.assertEqual(0, array.tail()) 111 | self.assertEqual(0, array.get(0)) 112 | self.assertEqual("out-of-range", array.get(1)) 113 | 114 | array.append(1) 115 | self.assertEqual(2, array.count) 116 | self.assertEqual(0, array.head()) 117 | self.assertEqual(1, array.tail()) 118 | self.assertEqual(0, array.get(0)) 119 | self.assertEqual(1, array.get(1)) 120 | self.assertEqual("out-of-range", array.get(2)) 121 | 122 | for i in range(array.count): 123 | array.set(i, array.count-i) 124 | self.assertEqual(array.count-i, array.get(i)) 125 | --------------------------------------------------------------------------------