Skip to content

Geekcreit DIY T300 NodeMCU WiFi controlled tank

Attila Magyar edited this page Mar 27, 2017 · 63 revisions

Introduction

This is a tutorial about controlling an esp8266 based caterpillar with a gamepad. The tank has an aluminum alloy chassis, 2 DC motors, L293D motor driver, and a doit NodeMCU development board.

I bought mine at banggood.

The built-in firmware uses a program written in Lua to receive UDP commands from a smartphone then translates those messages to GPIO commands.

We're going to write some code in Punyforth that will allow us to control the robot interactively either through a REPL, or using a standard USB gamepad.

Control

After studying the offical Lua script, I deleted the built-in firmware and recreated the control program in Punyforth.

The robot uses a L293D motor driver that can be controlled by 4 GPIO pins.

Pin configuration

The default pin setup looks as the following:

5 constant: PIN_SPEED_1 \ D1 leg
4 constant: PIN_SPEED_2 \ D2 leg
0 constant: PIN_MOTOR_1 \ D3 leg
2 constant: PIN_MOTOR_2 \ D4 leg

We have 2 pins for controlling the 2 DC motors, and 2 other pins for controlling the speed. For example writing GPIO_LOW to both of the motor pins makes the tank move forward with a certain speed. The speed can be adjusted by PWM signals sent to the speed control pins.

Controlling direction

We want to control the robot using Punyforth commands like this:

forward direction medium speed

Let's create a word for each of the directions.

: forward ( -- v1 v2 ) GPIO_LOW  GPIO_LOW  ;
: back    ( -- v1 v2 ) GPIO_HIGH GPIO_HIGH ;
: left    ( -- v1 v2 ) GPIO_LOW  GPIO_HIGH ;
: right   ( -- v1 v2 ) GPIO_HIGH GPIO_LOW  ;

As you can see, GPIO_HIGH, GPIO_LOW represents a right turn, because it makes motor1 spin backward and motor2 spin forward.

The word direction sets the given direction by writing digital values to the motor pins.

: direction ( v1 v2 -- )
    PIN_MOTOR_2 swap gpio-write
    PIN_MOTOR_1 swap gpio-write ;
forward direction \ makes the tank move forward with a certain speed

Controlling speed

Punyforth uses a software emulated PWM that is implemented in esp-open-rtos. The maximum value of a PWM duty cycle is 65535. This represents full speed. Unfortunately the esp-open-rtos implementation doesn't handle the maximum and minimum values correctly, so we're going to treat those values as special cases.

Let's create some predefined speed constants

30000 constant: very-slow
40000 constant: slow
50000 constant: medium
60000 constant: fast
65535 constant: full

The following word sets the given speed, but handles the maximum (65535) and minimum (0) speed values as special cases, as mentioned above.

: speed ( n -- )
    case
        0 of
            pwm-stop                         \ 0 is a special case, don't use PWM, just write LOW to each pins
            PIN_SPEED_1 GPIO_LOW gpio-write
            PIN_SPEED_2 GPIO_LOW gpio-write
        endof
        full of                              \ full is a special case, don't use PWM, just write HIGH to each pins
            pwm-stop
            PIN_SPEED_1 GPIO_HIGH gpio-write
            PIN_SPEED_2 GPIO_HIGH gpio-write        
        endof
        pwm-duty                              \ in general case just set the duty cycle
        pwm-start   
    endcase ;

Finally, we define a brake word that makes the robot stop by setting speed to zero.

: brake ( -- ) 0 speed ;

Now we can use this nicely readable syntax to control the robot. This example below, moves the robot forward (at fast speed) for 3 seconds.

forward direction fast speed \ makes the robot move forward at fast speed
3000 ms                      \ delays for 3 seconds
brake                        \ stop the robot
left direction very-slow speed
2000 ms
back direction full speed
5000 ms
brake

Using a gamepad

Controlling the robot interactively through the REPL (TCP, or serial) is fun, but being able to do the same using a gamepad sounds even better.

We're going to write a simple UDP server in Punyforth that will receive commands from a PC with a gamepad attached. With some Python script we'll translate the gamepad actions to UDP packets then forward them to the Punyforth server running on the esp8266.

UDP server on the esp

8000 constant: PORT                                        \ UDP server port
PORT wifi-ip netcon-udp-server constant: server-socket     \ this is the server socket that listens on the given UDP port
1 buffer: command                                          \ 1 byte buffer, because we're going to use 1 byte commands

: command-loop ( task -- )                                 \ the command loop task that receives commands from the socket
    activate    
    begin
        server-socket 1 command netcon-read                \ let's read 1 byte into the command buffer
        -1 <>
    while
        command c@                                         \ check the received byte
        case
            char: F of                                     \ in case character F we move the tank forward at current-speed
                forward direction current-speed @ speed
            endof
            char: B of
                back direction current-speed @ speed
            endof
            char: L of 
                left direction current-speed @ speed
            endof
            char: R of 
                right direction current-speed @ speed
            endof
            char: I of                          \ this will increase the speed by 1000
               current-speed @ 1000 + full min
               current-speed !
            endof
            char: D of 
                current-speed @ 1000 - 0 max
                current-speed !
            endof       
            char: S of brake endof
            char: E of engine-start endof
            char: H of engine-stop  endof            
        endcase
    repeat 
    deactivate ;

The above code fires up a UDP server then reads 1 byte commands in a loop. Then based on the command it controls the robot.

Python client that handles the gamepad

The only thing is missing is the Python code handles the gamepad actions. This code is a simple UDP client that sends commands to the given address.

class Tank:
    directions = {        
        (0, -1) : b'F',
        (0, 1)  : b'B',
        (-1, 0) : b'L',
        (1, 0)  : b'R',
        (0, 0)  : b'S'
    }    
    def __init__(self, address):
        self.address = address
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.engine_started = False
        
    def move(self, direction):
        if direction in Tank.directions:
            self._command(Tank.directions[direction])

    def speedup(self): self._command(b'I')
    def slowdown(self): self._command(b'D')
            
    def toggle_engine(self):
        self._command(b'H' if self.engine_started else b'E')
        self.engine_started = not self.engine_started
        
    def _command(self, cmd):
        print('Sending command %s to: %s' % (cmd, self.address))
        self.socket.sendto(cmd, self.address)

This code uses pygame to read gamepad inputs.

class Gamepad:
    def __init__(self, joystick, horizontal_axis, vertical_axis, button_config):
        pygame.init()
        pygame.joystick.init()        
        self.joystick = pygame.joystick.Joystick(joystick)
        self.button_config = button_config
        self.horizontal_axis, self.vertical_axis = horizontal_axis, vertical_axis        
        self.joystick.init()
        print("Joystick %s initialized" % self.joystick.get_name())
        
    def control(self, robot):        
        while True:
            for event in pygame.event.get():
                direction = [self.joystick.get_axis(self.horizontal_axis), self.joystick.get_axis(self.vertical_axis)]
                robot.move(tuple(map(round, direction)))
                if self._button_down('engine'):
                    robot.toggle_engine()
            if self._button_down('speed+'):
                robot.speedup()
            elif self._button_down('speed-'):
                robot.slowdown()

    def _button_down(self, name):
        return self.joystick.get_button(self.button_config[name]) == 1
button_config={'engine': 0, 'speed+': 5, 'speed-': 7}
gamepad = Gamepad(
    joystick=0, 
    horizontal_axis=0, 
    vertical_axis=1, 
    button_config=button_config)
gamepad.control(Tank(('192.168.0.22', 8000)))

You can see the final result in action here.

TankVideo

Full code is available here

Avoiding obstacles

Adding a simple auto pilot that makes the robot to avoid obstacles is very easy. We're going to use a cheap ultrasonic distance sensor (HC-SR04) to detect objects in front of the robot. Most of these sensors are 5V devices but you can find variations that work both with 3.3V and 5V. This makes the integration very easy. Besides Gnd and Vcc, these sensors have an echo pin and a trigger pin. The trigger pin is for emitting a short ultra sonic pulse, and the echo pin is for measuring the time taken the signal to come back to the sensor (this can be used to calculate the distance).

We're going to use the ping module to make the measurement.

13 constant: PIN_TRIGGER \ D7
12 constant: PIN_ECHO    \ D6

\ measure distance (maximum 100 cm)
PIN_ECHO 100 cm>timeout PIN_TRIGGER ping pulse>cm

We need to decide the maximum distance we're interested in. In this example this is 100 cm. This is because it takes time for the sound to come back to the sensor therefore we should specify the maximum amount of time we're willing to wait.

The ping word throws an exception if the distance is longer than the specified maximum. Our obstacle detector is only interested in if the distance is shorter than a specific value, so we can convert this exception to the maximum distance.

: distance ( -- cm | MAX_CM )
    { PIN_ECHO MAX_CM cm>timeout PIN_TRIGGER ping pulse>cm }
    catch dup ENOPULSE = if
        drop MAX_CM
    else
        throw
    then ;

The obstacle? word returns true if the distance is shorter than 30 cm.

: obstacle? ( -- bool ) distance 30 < ;

The logic in the auto pilot is very simple. We turn the robot until no obstacle is detected, then we go forward.

: auto-pilot ( -- )
    begin
        begin
            obstacle?
        while
            turn
        repeat
        go
    again ;
: turn ( -- ) 
    right direction medium speed 50 ms ;
    
: go ( -- ) 
    forward direction medium speed 50 ms ;

This is far from being the most intelligent or reliable way to avoid obstacles. This is because the distance sensor can't reliably detect an object which is not perpendicular to the sensor. A future improvement might be to either use multiple sensors in an angle or put one sensor on top of a rotating platform.

But considering how simple this was, the result is not that bad.

TankVideoObstacles

Attila Magyar 2016