-
Notifications
You must be signed in to change notification settings - Fork 43
Geekcreit DIY T300 NodeMCU WiFi controlled tank
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.
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.
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.
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
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
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.
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.
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.
Full code is available here
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.
Attila Magyar 2016
Attila Magyar