-
Notifications
You must be signed in to change notification settings - Fork 41
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 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.
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_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.
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 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
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.
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.
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.
Full code is available here
Attila Magyar 2016
Attila Magyar