Add Esp32 (serial) vehicle to pycar
This commit is contained in:
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 |
|
||||||
@@ -20,3 +22,7 @@ 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)
|
||||||
26
pycar/src/car/control/gpio/abstract_vehicle.py
Normal file
26
pycar/src/car/control/gpio/abstract_vehicle.py
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
42
pycar/src/car/control/gpio/serial_vehicle.py
Normal file
42
pycar/src/car/control/gpio/serial_vehicle.py
Normal 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]))
|
||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user