Add Esp32 (serial) vehicle to pycar

This commit is contained in:
Piv
2020-09-12 15:49:01 +09:30
parent fc3607316d
commit c97df7bbaa
8 changed files with 99 additions and 46 deletions

3
.vscode/launch.json vendored
View File

@@ -10,7 +10,8 @@
"request": "launch", "request": "launch",
"module": "car", "module": "car",
"env": { "env": {
"CAR_VEHICLE": "CAR_MOCK", "CAR_VEHICLE": "VEHICLE_MOCK",
// "CAR_VEHICLE": "VEHICLE_SERIAL",
// "CAR_LIDAR": "/dev/tty.usbserial-0001", // "CAR_LIDAR": "/dev/tty.usbserial-0001",
"CAR_LIDAR": "LIDAR_MOCK" "CAR_LIDAR": "LIDAR_MOCK"
} }

View File

@@ -10,6 +10,8 @@ The protocol specification is as follows, assuming an array of bytes:
| 0 | 0 if Calibrating a servo. Higher values indicates channel number to set duty cycle on. | | 0 | 0 if Calibrating a servo. Higher values indicates channel number to set duty cycle on. |
| 1 | If byte 0 = 0: number of servos to calibrate. Else, the new duty cycle value for the channel specified in byte 0. | | 1 | If byte 0 = 0: number of servos to calibrate. Else, the new duty cycle value for the channel specified in byte 0. |
When setting the duty cycle, the current min angle is 0, and the max angle is 255, which allows the entire byte to be used for setting the duty cycle.
The table below describes the byte format for calibrating a servo. This array of bytes can be repeated for the given number of servos in byte 1: The table below describes the byte format for calibrating a servo. This array of bytes can be repeated for the given number of servos in byte 1:
| Byte Number | Value Description | | Byte Number | Value Description |
@@ -19,4 +21,8 @@ The table below describes the byte format for calibrating a servo. This array of
The min/max pulse widths are hardcoded to 1000us/2000us respectively. The min/max pulse widths are hardcoded to 1000us/2000us respectively.
At the end of each loop, the entire array will be read, to flush any data that may cause issues later. At the end of each loop, the entire array will be read, to flush any data that may cause issues later.
Upcoming (TODO):
- Use bit shift to allow 12 bits to be used for the duty cycle range, as there can only be a max of 16 channels anyway (4 bits).
- Consider protobuf or msgpack for serialisation format, for more advanced use cases, and more maintainable communication formats (currently changing the message format will require changes to the entire protocol)

View File

@@ -0,0 +1,26 @@
from abc import ABC, abstractmethod, abstractproperty
class AbstractVehicle(ABC):
@abstractmethod
@property
def throttle(self):
pass
@abstractmethod
@throttle.setter
def throttle(self):
pass
@abstractmethod
@property
def steering(self):
pass
@abstractmethod
@steering.setter
def throttle(self):
pass
def stop(self):
pass

View File

@@ -1,18 +1,25 @@
from car.control.gpio.abstract_vehicle import AbstractVehicle
from car.control.gpio.serial_vehicle import SerialVehicle
from .mockvehicle import MockVehicle from .mockvehicle import MockVehicle
import os import os
def get_vehicle(motor_pin=19, steering_pin=18): # TODO: Remove need for motor/steering pin, instead retrieve from env variable.
# TODO: Dependency injectino in python?
def get_vehicle(motor_pin=19, steering_pin=18) -> AbstractVehicle:
ENV_CAR = None if 'CAR_VEHICLE' not in os.environ else os.environ['CAR_VEHICLE'] ENV_CAR = None if 'CAR_VEHICLE' not in os.environ else os.environ['CAR_VEHICLE']
if ENV_CAR == "CAR_2D": if ENV_CAR == "VEHICLE_2D":
try: try:
from .vehicle import Vehicle from .vehicle import Vehicle
return Vehicle(motor_pin, steering_pin) return Vehicle(motor_pin, steering_pin)
except ImportError: except ImportError:
print( print(
'Could not import CAR_2D vehicle. Have you installed the GPIOZERO package?') 'Could not import CAR_2D vehicle. Have you installed the GPIOZERO package?')
elif ENV_CAR == "CAR_MOCK": elif ENV_CAR == "VEHICLE_MOCK":
return MockVehicle(motor_pin, steering_pin) return MockVehicle()
elif ENV_CAR == "VEHICLE_SERIAL":
# TODO: Pins in environment variables.
return SerialVehicle()
else: else:
print('No valid vehicle found. Have you set the CAR_VEHICLE environment variable?') print('No valid vehicle found. Have you set the CAR_VEHICLE environment variable?')
return None return None

View File

@@ -1,10 +1,11 @@
# A dummy vehicle class to use when # A dummy vehicle class to use when testing/not connected to a real device.
class MockVehicle: from car.control.gpio.abstract_vehicle import AbstractVehicle
def __init__(self, motor_pin=19, servo_pin=18):
self.motor_pin = motor_pin
self.steering_pin = servo_pin class MockVehicle(AbstractVehicle):
def __init__(self):
print('Using Mock Vehicle') print('Using Mock Vehicle')
@property @property
@@ -23,21 +24,5 @@ class MockVehicle:
def steering(self, value): def steering(self, value):
self._steering = value self._steering = value
@property
def motor_pin(self):
return self._motor_pin
@motor_pin.setter
def motor_pin(self, value):
self._motor_pin = value
@property
def steering_pin(self):
return self._steering_pin
@steering_pin.setter
def steering_pin(self, value):
self._steering_pin = value
def stop(self): def stop(self):
self.throttle = 0 self.throttle = 0

View File

@@ -1,11 +1,12 @@
import datetime import datetime
from .abstract_vehicle import AbstractVehicle
class VehicleRecordingDecorator: class VehicleRecordingDecorator(AbstractVehicle):
def __init__(self, vehicle): def __init__(self, vehicle):
""" """
A decorator for a vehicle object to record the changes in steering/throttle. A decorator for a vehicle object to record the changes in steering/throttle.
This will be recorded to memory, and will save to the given file when save is called. This will be recorded to memory, and will save to the given file when save_data is called.
Parameters Parameters
---------- ----------
@@ -66,21 +67,5 @@ class VehicleRecordingDecorator:
's,' + str(value) + ',' + datetime.datetime.now().isoformat(sep=' ', timespec='seconds')) 's,' + str(value) + ',' + datetime.datetime.now().isoformat(sep=' ', timespec='seconds'))
self._vehicle.steering = value self._vehicle.steering = value
@property
def motor_pin(self):
return self._vehicle.motor_pin
@motor_pin.setter
def motor_pin(self, value):
self._vehicle.motor_pin = value
@property
def steering_pin(self):
return self._vehicle.steering_pin
@steering_pin.setter
def steering_pin(self, value):
self._vehicle.steering_pin = value
def stop(self): def stop(self):
self.throttle = 0 self.throttle = 0

View File

@@ -0,0 +1,42 @@
from .abstract_vehicle import AbstractVehicle
from serial import Serial
STEERING_CHANNEL = 1
THROTTLE_CHANNEL = 2
class SerialVehicle(AbstractVehicle):
def __init__(self, serial_port='/dev/ttyUSB0', steering_pin=12, throttle_pin=14):
self.serial_port = Serial(port=serial_port, baudrate=115200)
# Initialise the channels and pins on esp32.
self._init_esp32_pwm(steering_pin, throttle_pin)
self.throttle = 0
self.steering = 0
@property
def throttle(self) -> float:
return self.throttle
@throttle.setter
def throttle(self, new_throttle: float):
self.throttle = new_throttle
self._set_servo_value(THROTTLE_CHANNEL, new_throttle)
@property
def steering(self) -> float:
return self.steering
@steering.setter
def steering(self, new_steering: float):
self.steering = new_steering
self._set_servo_value(STEERING_CHANNEL, new_steering)
def _set_servo_value(self, channel, value):
# Scale the value to a byte, as 0-255 is the angle range for the esp32 servo.
self.serial_port.write(bytes[channel, (value + 1) / 2 * 255])
def _init_esp32_pwm(self, steering_pin, throttle_pin):
self.serial_port.write(bytes([0, 2, STEERING_CHANNEL,
steering_pin, THROTTLE_CHANNEL, throttle_pin]))

View File

@@ -1,3 +1,4 @@
from .abstract_vehicle import AbstractVehicle
from gpiozero import Servo, Device from gpiozero import Servo, Device
from gpiozero.pins.pigpio import PiGPIOFactory from gpiozero.pins.pigpio import PiGPIOFactory
import subprocess import subprocess
@@ -29,7 +30,7 @@ def _is_pin_valid(pin):
# two servos for controls (e.g. drone, dog) # two servos for controls (e.g. drone, dog)
class Vehicle: class Vehicle(AbstractVehicle):
def __init__(self, motor_pin=19, servo_pin=18): def __init__(self, motor_pin=19, servo_pin=18):
subprocess.call(['pigpiod']) subprocess.call(['pigpiod'])
Device.pin_factory = PiGPIOFactory() Device.pin_factory = PiGPIOFactory()