Creating a Platform for a Self-Driving Skatepark with Python

Ever since I first played Paperboy as a kid, I’ve wanted a way to transform the streets of suburbia into something more exciting. My current neighborhood has a sidewalk along the entire street, and I often take my dog on runs, where he pulls me on my skateboard.

When we ride together, I’ve often wished there were ramps sprinkled at houses along the way, like in Paperboy. On one of these runs, the thought occurred to me that I could possibly build a skate ramp on a motorized platform to ride with us. This idea grew, and eventually I came up with the goal to make an entire self-driving skatepark that could journey with you to your destination, ramps taking turns stopping and getting back in front of you along the way.

In today’s blog post we’ll step through the process so far, from idea to a few prototypes, and how I’ve built a platform for remote controlled skate ramps. At the end there will be a Github repository if you want to help out, or try building your own.

With that, let’s get started!

Architecture of a Self-Driving Skate Ramp

When I first got the idea for a self-driving skate ramp, I immediately thought of a golf cart being repurposed into a ramp on wheels. There was something very appealing about the absurdity of a gigantic skate ramp driving down the street.

But as I looked for prior art, I quickly realized that the linear actuator and mechanics of the steering system seemed like a bit too much of an investment to get to a prototype to test an idea. So instead, I focused on a mix between an electric skateboard platform and the NVIDIA JetRacer.

The NVIDIA JetRacer is a Jetson Nano powered self-driving RC car. It runs on ROS, and follows a basic track around a loop, using computer vision. It seemed promising as prior art.

Really, the JetRacer was a backup to prove that if I could get the hardware down, the software should be implementable, given the performance of JetRacers. (It also helped that I have two Jetson Nanos laying around, with the shortage of chips everywhere.)

With that decision, it was time to figure out how to get started. So I went on eBay and bought a broken electric skateboard to begin my journey.

Controlling Electric Skateboard Motors in Software

I spent some time last year building FPV drones, but when I began this project didn’t have any familiarity with electric skateboards. Luckily, it seems there’s an open source movement for their motor control systems, similar to the one found on hobbyist drones.

Electric skateboards mostly use BDLC motors to provide an enormous amount of power in combination with LiPo batteries. The trio of motor controller software, motor, and LiPo batteries is largely what powers the eBike, eScooter, eSkate, and drone markets. So the architecture across platforms is awfully similar.

Getting the motors controlled via USB in Python was straightforward enough. I hooked up my benchtop power supply, strapped down my skateboard trucks, and was off to the races with the pyVESC library:

import pyvesc

serial_port = '/dev/ttyACM1'
front_motor = pyvesc.VESC(serial_port=serial_port)

while True:
  front_motor.set_rpm(3000)
  sleep(10)
  front_motor.set_rpm(0)

My original plan for steering the platform was to use two different motors, mounted on opposite sides of my skateboard. Depending on which direction I needed to turn, I’d spin them at different speeds.

This really didn’t work well in practice at all. When I built a first version, I had problems with the inconsistency of grip with my wheels, couldn’t use it to reliably steer at all. Instead, my ramp kind of hopped and spun out in either direction. That wouldn’t work for driving in and out of sidewalks.

Another problem I had was with the synchronization of my motors. When I first tried out my platform, I hooked both motors up via USB. This led to a bit of a delay in activation for the motors, and a slight difference in speeds. To fix this, I wired the controllers in a master / slave configuration with CANBUS.

This proved to be a bit tricky to do over USB, as it wasn’t very well documented, and was completely unsupported by PyVesc. So I ended up searching a bit, and eventually found the command to send a CANBUS slave message over USB (and control the second controller via a first controller):

class SetRPM(metaclass=pyvesc.VESCMessage):
    """
    Sets the RPM on a CANBUS connected device. 
    Messages have to be sent repeatedly, as CANBUS has a timeout that's configurable
    within the VESC application. In my case, I set it to 5 seconds, which means the
    motor only works for 5 seconds after sending this signal.
    """
    id = 34
    fields = [
        ('motor_id', 'B'), # my slave is set to 1
        ('command', 'B'), # 8, thanks to this page: https://www.vesc-project.com/node/774
        ('rpm', 'i') # because we're assuming RPM setting
    ]

Because of the requirements for rider safety, you can’t just send a single command via CANBUS on the VESC platform. Instead, you need to continuously send the message.

In order to do this, I had to make some updates to the way my Python program ran. I added a thread to send a heartbeat of the current message:

def send_target_message():
    myData = threading.local()
    myData.drive_speed = 0
    while True:
        try:
            val = queue.get(block=False)
            myData.drive_speed = val
        except Exception as e:
            time.sleep(.001)
        front_motor.write(encode(SetRPM(1, 8, myData.drive_speed)))
        time.sleep(.001)
        front_motor.set_rpm(myData.drive_speed)

controller = RampController(interface="/dev/input/js0", connecting_using_ds4drv=False)
t = threading.Thread(target=send_target_message)
t.start()
controller.listen()

With this, I’m able to send messages to both controllers via a single USB connection.

Learning from the First Iteration Prototype

Despite the obvious shortcomings, I took my ramp for a spin. In practice, it ended up being much scarier than I’d anticipated.

Normal skate ramps don’t move at all. In this case, when I hit the ramp, it moved on two axis. One towards me, and the other side to side. Between this and the lack of steering, it meant I didn’t really have a ramp capable of steering itself. At best, I’d have a ramp I could use to pull me to a place, and try to steer by leaning on.

My next version had to have some sort of way to put the ramp on the ground completely for when I skated it, and also be reasonably steerable. Both of these led me to my first metalworking projects.

Bringing Steering and Ramp Lifting / Lowering to the Platform

Given the failures, I didn’t want to give up the ecosystem of parts available with electric skateboards with the second prototype. So I settled on trying to attach a linear actuator to the trucks on the skateboard for steering, using a motor mount.

This worked surprisingly well, but built up some serious forces on my screws, and ripped them right out. So I realized I needed to make some bolts go through the plywood base, to keep the linear actuator from ripping the mounts right off.

This led me to the next problem, how to raise and lower the ramp itself. It seemed like overkill to get another linear actuator, and try to fabricate another mount. Instead, I went looking, and found an interesting electric jack on Amazon.

I ordered the jack, and decided to fabricate a metal mount between the skateboard and the ramp.

This was my first welding attempt ever, and also my first time using a metal brake. Both mostly went off without a hitch for the first round. YouTube helped with learning MIG welding, along with a low-cost Harbor Freight flux-core welder. (I’d always wanted to learn welding, but assumed it would be thousands of dollars to get started.)

Once the parts were fabricated, the only thing was left to build the software to control a linear actuator and an electric jack.

Luckily, both of these are basically the same, simple circuit. You put electricity in one way for up, down for the other. Two relays wired correctly are enough to control the linear actuator going out or in, and the jack going up or down. We can control the relays with a Teensy microcontroller and setting two pins either HIGH or LOW for each actuator:

#include <Packet.h>
#include <PacketCRC.h>
#include <SerialTransfer.h>

SerialTransfer steerTransfer;

const long interval = 1000;
unsigned long previousMillis = 0;
int ledState = LOW;

struct __attribute__((packed)) STRUCT {
  char d;
  int x;
} steerStruct;

void setup() {
  Serial.begin(115200);
  steerTransfer.begin(Serial);
  // left and right
  pinMode(A9, OUTPUT);
  pinMode(A8, OUTPUT);
  // up and down
  pinMode(A7, OUTPUT);
  pinMode(A6, OUTPUT);
  pinMode(13, OUTPUT);
  digitalWrite(A9, LOW);
  digitalWrite(A8, LOW);
  digitalWrite(A7, LOW);
  digitalWrite(A6, LOW);
}

void loop() {
 unsigned long currentMillis = millis();

 if (currentMillis - previousMillis >= interval) {
  previousMillis = currentMillis; 
  // blink if teensy has power
  if (ledState == LOW) {
    ledState = HIGH;
  } else {
    ledState = LOW;
  }
  digitalWrite(13, ledState);
 }
 
 if(steerTransfer.available()) {
  uint16_t recSize = 0;
  recSize = steerTransfer.rxObj(steerStruct, recSize);
  if (steerStruct.d == 'R') {
    if (steerStruct.x == 1) {
      digitalWrite(A9, HIGH);
    }
    else {
      digitalWrite(A9, LOW);
      digitalWrite(A8, LOW);
    }
  }
  else if (steerStruct.d == 'L') {
    if (steerStruct.x == 1) {
      digitalWrite(A8, HIGH);
    }
    else {
      digitalWrite(A9, LOW);
      digitalWrite(A8, LOW);
    }
  }
    else if (steerStruct.d == 'U') {
      if (steerStruct.x == 1) {
        digitalWrite(A6, HIGH);
      }
      else {
        digitalWrite(A7, LOW);
        digitalWrite(A6, LOW);
      }
    }
    else if (steerStruct.d == 'D') {
      if (steerStruct.x == 1) {
        digitalWrite(A7, HIGH);
      }
      else {
        digitalWrite(A7, LOW);
        digitalWrite(A6, LOW);
      }
    }
 }
}

If you read the code, you’ll notice I have commands that I’m sending to Arduino from Python, U or D for up or down, and L or R for left and right. To send these custom commands from Python, I used one of my new favorite libraries, SerialTransfer.

It allows you to specify a custom command to send, and it just works. Sending my message from Python is literally this easy:

from pySerialTransfer import pySerialTransfer

link = pySerialTransfer.SerialTransfer('/dev/ttyACM0')
link.open()

# define our data structure (direction and on or off)
class dataStruct(object):
    d = 'L'
    x = 1

# send the up command on 
testStruct = dataStruct
testStruct.d = 'U'
testStruct.x = 1
sendSize = 0
sendSize = link.tx_obj(testStruct.d, start_pos=sendSize)
sendSize = link.tx_obj(testStruct.x, start_pos=sendSize)
link.send(sendSize)

On the bench, this setup works great. The ramp platform is super responsive, and has about as great of performance as I could ask for. But in practice, once I throw a ramp on top of the platform, there’s still so much to do.

Debugging a Remote Control Ramp

Once a ramp is placed on the robot, the platform itself becomes largely inaccessible. This means debugging can be especially tricky, given the wireless setup. Sometimes there seems to be a delay in bluetooth controlling the robot itself. Again, of right now, the platform is controlled with a PS4 controller via a Bluetooth connection to the Jetson Nano.

This ended up being a challenge on my maiden voyage with the new platform. As I drove the ramp into place, I pressed the button on the PS4 controller to lower the ramp. Nothing happened, so I pressed it again. Still nothing. Curious, I started walking to the ramp, only to have it start getting lowered, and then continue lowering itself until the jack ripped the threadscrew out of itself entirely and broke.

Disaster! This meant I had to completely refabricate the jack and mount, which really is the most work of any component on the ramp. This time, I added limited switches to my Arduino code. (But if you look close, they still don’t work!)

Choosing a PS4 controller means I have a limited range for communication with the platform, especially when the ramp is on it. One of my next moves will be to put a real radio controller on it, along with a proper RC controller.

Initially, I had my Jetson connect via Wifi to my home network. I’ve since added Tailscale to the Jetson Nano, and made it a permanent machine. This allows me to remotely access it from any network. I then added a long lived ephemeral key to Gitpod as a secret key. With this, I’m able to remotely log in and debug from a Wifi hotspot on my iPhone. This means I can remotely debug, and run longer distances with my ramp, away from the home as long as there’s cellular reception. (And my cell phone is around.)

A Second, Successful Voyage

Finally, I rebuilt my ramp jack, and wired up limit switches to prevent disaster. And, last Friday I was finally able to raise, drive, lower, and skate my ramp! There’s still a lot to be done before we get to self-driving, but it hey, let’s celebrate this milestone.

As always, the repository for the project is on Github, and has some more technical details. You should be able to mostly run with the project, as the code itself is pretty straightforward for now, and launchable within Gitpod. (You obviously won’t be able to connect to my Tailscale network.)

Just in case, the steps to get it running are: connect power for the Jetson and linear actuators, connect the PS4 controller via Bluetooth, connect the 10s battery power to the ESCs, and then run the loop_with_canbus.py Python script. Then the pressing R1 on the PS4 disables the safety switch, and allows you to steer / control the platform.

If you’d like to be notified of the next blog post, enter your info below:

Updated: