Remote Control LEGO Race Car

Introduction

 

In this tutorial, we show you how to QikEasy Adapter’s Virtual Wireless Sensor feature to build a custom remote controller to wirelessly control a LEGO Race Car.

 

Materials

 

The materials we used include:

 

For the Race Car:

    • LEGO Spike Prime or Robot Inventor set for building the race car
    • A QikEasy Adapter board

 

For the Remote Controller:

    • An ESP8266 development board
    • A 5V USB battery pack for powering the ESP8266
    • A Potentiometer
    • A Push button
    • A 3D printed shell to house all the components for the remote Controller

The LEGO Race Car

 

The actual construction design of your LEGO car do not matter much.  But because our custom remote controller sends data inputs to the hub as two channels: one for forward backward, and another for steering angle, it would make the implementation of the control coding simpler if the car uses 1 motor to drive forward and backward and uses another motor for the steering angle.

 

Make sure the QikEasy Adapter has been configured as a Virtual Wireless Sensor.  If you haven’t already done that, please follow the instructions here to perform the configuration.

The Word Block program running on the LEGO hub

 

The hub has 3 devices connected to it:

 

    • Port A: The QikEasy Adapter running as the Virtual Wireless Sensor where steering data comes in as Raw Red color intensity, and forward/stop data comes in as Raw Green color intensity.
    • Port B: Steering motor
    • Port C: Rear Wheel driving motor

 

This program simply sets the steering angle based on the Virtual Sensor’s red intensity.  And for when Raw Green intensity equals 1024, the driving motor will start, else it will stop.

 

Note: This program runs on Spike App 3, you will have change the range of the Raw Red and Raw Green from (0, 1024) to (0, 255) when using Spike App 2 or Robot Inventor.

 

Note #2: Make sure your QikEasy Adapter has been setup to run in Virtual Wireless Sensor mode as described on this page.

Building the Remote Control Hardware

 

Parts :

 

    • An ESP8266 Dev Board on a breadboard
    • A potentiometer
    • A Push button
    • A 5V USB battery pack for powering the ESP8266
    • 3D printed parts for holding everything together

How it works:

 

The potentiometer is used to provide variable analog voltage to the AD input (i.e. A0 pin) of the ESP8266, controlling the steering of the RC car.

 

A push button is used for controlling the ON/OFF state of the Rear Wheel driving motor.  We connected the push button input to the GPIO4 pin of ESP8226, which is set to PULL UP internally. D2 pin When push button is pressed, it would connect the GPIO4 pin to ground.

The Remote Controller Software

 

The software is basically the firmware running on the ESP8266 device.

 

Notes on the Program

  1. Remember to set the following at the beginning of the program:
      • the IP address of your QikEasy Virtual Wireless Sensor
      • the Wifi SSID and password
  2. Make sure to install the AsyncHTTPRequest_Generic (by Bob Lemaire and Khoi Hoang ) and its dependent libraries before building this ESP8266 firmware.

  3. The reason why we use AsyncHTTPRequest_Generic to make HTTP requests instead of the default ESP8266HTTPClient class is that when we use ESP8266HTTPClient to continuously make requests, we found that a lot of times, the requests will get stuck and that cause a long delay between requests.  This is unacceptable for our application which requires close to real time responses.

  4. With AsyncHTTPRequest_Generic, we have configured it to allow up 5 concurrent requests to be sent to our QikEasy Adapter simultaneously.  This is how we are able to handle these continuous requests.
  5. Note also that the reading from analogRead(A0) will need to be calibrated based on the potentiometer you are using and the turning radius you want to utilize on the potentiometer.
  6. The HTTP request sent to the QikEasy Virtual Sensor is in this format:

 

    http://<QIKEASY_IP_ADDR>/set?t=c&r=<Steering Value>&g=1024

 

Steering value ranges from 0 to 1024, where a value of 512 will align the wheel to go straight.

//****************************************************************
// Make sure to install the AsyncHTTPRequest_Generic and
// dependent libraries before building this.
//****************************************************************

#include &lt;ESP8266WiFi.h&gt;
#include &lt;AsyncHTTPRequest_Generic.h&gt;    // https://github.com/khoih-prog/AsyncHTTPRequest_Generic

#include &lt;Ticker.h&gt;


#define BUTTON_PIN 4
#define QIKEASY_IP_ADDR "192.168.11.122"   // &lt;&lt;----- Change this to the IP address of your QikEasy Virtual Sensor


#define SSID "&lt;YOUR SSID&gt;"
#define WIFIPASSWORD "&lt;YOUR WIFI PASSWORD&gt;"


int previous_button_state = 1;
unsigned long lastDebounceTime = 0;  // the last time the output pin was toggled
unsigned long debounceDelay = 10;    // the debounce time; increase if the output flickers
unsigned long updateSpeedInterval = 200;    // the debounce time; increase if the output flickers
int currentAnalogValue=-1;


#define MaxHttpClientConnection 5

struct concurrentConnection {
  AsyncHTTPRequest *request=NULL; 
  bool available=true;
} concurrentConnection[MaxHttpClientConnection];




// return -1 if no available http client is found
int findAvailableClientIndex() {
  for (int i=0; i&lt;MaxHttpClientConnection; i++)
    if (concurrentConnection[i].available)
      return i;
  return -1;
}

// return -1 if no matching http request client is found
int findClientIndexByAsyncHTTPRequest(AsyncHTTPRequest *pRequest) {
  for (int i=0; i&lt;MaxHttpClientConnection; i++) if (concurrentConnection[i].available == false &amp;&amp; concurrentConnection[i].request == pRequest ) return i; return -1; } void requestCB(void* optParm, AsyncHTTPRequest* thisRequest, int readyState) { (void) optParm; if (readyState == readyStateDone) { // Serial.println(F("Response Code = "), request-&gt;responseHTTPString());

    if (thisRequest-&gt;responseHTTPcode() != 200)
      Serial.println("Response error");
    //else
    //  Serial.println(request-&gt;responseText());

    int index=findClientIndexByAsyncHTTPRequest(thisRequest);
    if (index != -1) {
      delete concurrentConnection[index].request;
      concurrentConnection[index].available=true;    
    }
  }
}


void httpRequestToQikEasyAdapter(uint16_t red, bool pressed) {
  static bool requestOpenResult;
    
  if (WiFi.status() == WL_CONNECTED) {
    String url = String("http://") + QIKEASY_IP_ADDR + "/set?t=c&amp;r=" + red + "&amp;g=" + (pressed?1024:0);
    char myUrl[50];
    url.toCharArray( myUrl, 50);

    // Making the request.
    int httpClientIndex = findAvailableClientIndex();
    if (httpClientIndex != -1) {
      // Serial.printf("Connection index = %d\n", httpClientIndex);
      concurrentConnection[httpClientIndex].available=false;
      concurrentConnection[httpClientIndex].request = new AsyncHTTPRequest;
      concurrentConnection[httpClientIndex].request-&gt;onReadyStateChange(requestCB);
      requestOpenResult = concurrentConnection[httpClientIndex].request-&gt;open("GET", myUrl);
      //Serial.println(myUrl);
      if (requestOpenResult) {
        // Only send() if open() returns true, or crash
        concurrentConnection[httpClientIndex].request-&gt;send();
      }
      else
      {
        Serial.println("Can't send bad request");
        delete concurrentConnection[httpClientIndex].request;
        concurrentConnection[httpClientIndex].available=true;
      }
    }
  }
}
  

void setup() {
  Serial.begin(74880);
  WiFi.mode(WIFI_STA);
  WiFi.begin(SSID, WIFIPASSWORD);
  Serial.println("Waiting for Wifi Connection");
  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    delay(1000);
    ESP.restart();
  }
  Serial.println("Wifi Connected");
  
  pinMode(BUTTON_PIN, INPUT_PULLUP);
}



void loop() {
 int reading = digitalRead(BUTTON_PIN);

  int voltageValue = analogRead(A0);    // range: 500 to 1000, center point = 750
  bool analogValueChanged = ( abs(voltageValue - currentAnalogValue) &gt; 10 );
  
  if ( (reading != previous_button_state &amp;&amp; (millis() - lastDebounceTime) &gt; debounceDelay) ||
       (analogValueChanged &amp;&amp; (millis() - lastDebounceTime) &gt; updateSpeedInterval) ) {   // keep updating speed when button pressed

      // uint16_t red = (voltageValue-500)*2 + 12;  // For center point=750,  (750-500)*2 + 12 = 512 the desired center point.
      int16_t red = (voltageValue-500)*8 - 1488;     // For center point=750,  (750-500)*8 - 1488 = 512 the desired center point.
      if (red &lt; 0) red = 0; if (red &gt; 1024) red = 1024;

      // NOT PRESSED
      if (reading == HIGH)
        httpRequestToQikEasyAdapter(red, false);

      // PRESSED
      else
        httpRequestToQikEasyAdapter(red, true); 
  

      previous_button_state = reading;
      currentAnalogValue = voltageValue;
      lastDebounceTime = millis();
  }

  delay(100);
}

The following video explains how to construct this project.