Skip to content

Geekcreit DIY T300 NodeMCU WiFi controlled tank

Attila Magyar edited this page Sep 3, 2016 · 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 than translates those messages to GPIO commands.

We're going to write some code in Punyforth that will allow us to control the robot interactively 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_HIGH 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 a special case.

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

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 translates the gamepad actions to UDP packets than forward 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 to 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 ] literal of                         \ in case character F we move the tank forward at current-speed
                forward direction current-speed @ speed
            endof
            [ char: B ] literal of
                back direction current-speed @ speed
            endof
            [ char: L ] literal of 
                left direction current-speed @ speed
            endof
            [ char: R ] literal of 
                right direction current-speed @ speed
            endof
            [ char: I ] literal of                          \ this will increase the speed by 1000
               current-speed @ 1000 + full min
               current-speed !
            endof
            [ char: D ] literal of 
                current-speed @ 1000 - 0 max
                current-speed !
            endof       
            [ char: S ] literal of brake endof
            [ char: E ] literal of engine-start endof
            [ char: H ] literal of engine-stop  endof            
        endcase
    repeat 
    deactivate ;

The above code fires up a UDP server than reads 1 byte commands in a loop. Then based on the received 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

You can see the final result in action here.

TankVideo

Full code is available here

Attila Magyar 2016