-
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_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 translates 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 ] 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 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
Attila Magyar 2016
Attila Magyar