Wild Thumper based ROS robot

My ROS (Robot Operating System) indoor & outdoor robot

Similar projects worth following
I've build a few robots before, but never was able to do real navigation with them, e.g. drive successfully from living room to kitchen. Reason was that a few infrared and sonar sensors are not enough to do 'real' navigation. You can avoid obstacles with them, but not much more. My Software was not much better suited for this until I discovered ROS (Robot Operating System) with the rviz GUI a few years ago. Since ready available robots with ROS are a little bit expensive I decided to build a new robot in spring 2015 around the wild thumper 4wd chassis. The hardware and electronics were almost completed in a few weeks. Since then I've spent some time with tuning..

The goal is to be able to navigate indoor doing SLAM (simultaneous localization and mapping) and outdoor with GPS.

The free space on the back is supposed to host a robot arm somewhere in the future.


  • Wild Thumper 4wd chassis
  • Motors upgraded with encoders
  • Total weight: 3.3kg

Power supply:

  • Battery: 2x 7.2V NiMh, fused with 30A (slow)
  • 5V via voltage regulator D24V50F5 (5A), fused with 3A (fast)

The two batteries are connected in parallel using a LM5050-2 active ORing circuit each. Another LM5050-2 can be connected in parallel for docking station supply.


  • Solid Run Hummingboard (i.MX6 ARM Cortex-A9 Dual Core 1GHz, 2GB RAM)
  • AVR Atmega32 for motor control
  • AVR Atmega328 (Arduino Nano) for I/O


  • Hummingboard: GPS (uart), IMU (USB), 3D-Camera (USB), 2xAVR over I2C, PCA9517 "Level translating I2C-bus repeater" to bridge the 3.3V with the 5V I2C.

Motor control:

  • Motors driven by 4x VNH2SP30, one for each on a 20kHz PWM
  • Speed control (PID) and Odometry from wheel encoders are calculated on Atmega328 (yes, doing float on it).

Inputs/Outputs on Atmega328:

  • 3x distance sonar sensors, 2x infrared distance sensors, battery voltage

Odometry calculation:

  • Odometry from wheels corrected with Tinkerforge IMU Brick 2.0 with Kalman filtering


  • Xtion Pro Live depth camera
  • 2x IR 2D120X (1x left, 1x right)
  • 3x sonar SRF05 (2x front, 1x aft).

The point of the sonar sensors is to correct the dead zone of the depth camera in less then 0.5m


  • Debian Stretch
  • Robot Operating System (ROS) Kinetic

Object following:

With ultra-wideband (UWB) modules the robot can follow a target, in the following video a R/C car:

Details in the corresponding log "Follow me - part 4".

GPS test video:

The following videos shows the robot automatically driving a square by four GPS waypoints. The front camera is shown on the lower left, the rviz map video is shown on the upper left.

LED stripe demo:

  • 1 × Wild Thumper 4wd
  • 2 × Battery NiMh 7.2V 5000mAh
  • 2 × LM5050-2 board active ORing Custom board
  • 1 × D24V50F5 5V/5A voltage regulator
  • 1 × Solid Run HummingBoard-Pro + i.MX6 Quad SoM with W-Lan & Bluetooth

View all 10 components

  • Generating a Wi-Fi heatmap

    Humpelstilzchen12/26/2018 at 16:19 0 comments

    Something useful for my robot to do: Having troubles with the Wi-Fi at my parents house I decided to create a heatmap of the W-Lan signal strength with the inspiration of this ROSCon 2018 talk. Since I did not understand QGIS in a few minutes and because my use case was rather limited I created the map based on OpenLayers instead. An example can be seen in the following image created for my smale place:

    Given an already created floor plan with SLAM I drove my robot around and took a measurement of the Wi-Fi signal strength every 0.5 second. The signal strength ranges from green (very good) to red (very poor). Since I do not have any problems with my Wi-Fi all dots are more or less green for my place. From these discrete measurements a contour plot is generated. In this contour plot it can be easily seen that the access point is in the lower left corner (darkest green).

    The steps to reproduce are the following:

    1. Drive around the apartment and take a measurement twice a second. The measurements are saved in a JSON file. This is done with the following quick & dirty python script:

    #!/usr/bin/env python
    # -*- coding: iso-8859-15 -*-
    import os
    import re
    import rospy
    import tf
    import tf2_ros
    from time import sleep
    tfBuffer = tf2_ros.Buffer()
    listener = tf2_ros.TransformListener(tfBuffer)
    regex_lq = re.compile("Link Quality=(\d*)/(\d*)")
    while not rospy.is_shutdown():
        f = os.popen("export LANG=C; /sbin/iwconfig wlan0")
        for line in f:
            line = line.strip()
            match_lq = regex_lq.match(line)
            if match_lq is not None:
                lq = float( / float(
                pos = tfBuffer.lookup_transform("map", 'base_link', rospy.Time(0), rospy.Duration(1.0))
                print '{"x":%.2f, "y":%.2f, "link":%.2f},' % (pos.transform.translation.x, pos.transform.translation.y, lq)

    The python script does the following

    • Retrieve the current position with ros/tf
    • Read the signal strength of the connected access point with a regular expression from iwconfig (value: Link Quality)
    • Append position (x,y) and Link Quality to a JSON array, example:
      {"x":0.32, "y":-0.15, "link":0.69},
      {"x":0.34, "y":-0.12, "link":0.73},
      {"x":0.38, "y":-0.05, "link":0.74}

     2. Generate a heatmap with matplotlib contourf

    The data is first interpolated from the measurements with 300 steps in each direction from minimum to maximum with linspace and griddata, the heatmap is generated with contourf. The size of 10x10 meter easily covers my whole apartment.

    #!/usr/bin/env python
    # -*- coding: iso-8859-15 -*-
    import json
    import matplotlib.pyplot as plt
    import numpy as np
    from scipy.interpolate import griddata
    # settings
    size = (10, 10) # meter x meter
    contour_steps = 300
    resolution= 0.025 # m/px
    # ----------
    lx = []
    ly = []
    llink = []
    dpi=100.0/(resolution*100)*2.54 # scale 100dpi to our resolution in m/px
    with open("wifi_strength.json", "r") as read_file:
        data = json.load(read_file)
        for line in data:
        # for a square image both sizes must be equal
        minimum = min(lx) if min(lx) < min(ly) else min(ly)
        maximum = max(lx) if max(lx) > max(ly) else max(ly)
        if abs(minimum) > abs(maximum):
            maximum = -abs(minimum)
            minimum = -abs(maximum)
        # figure without margin
        fig=plt.figure(num=None, figsize=size, dpi=dpi)
        ax.set_xlim(-size[0]/2, size[0]/2)
        ax.set_ylim(-size[1]/2, size[1]/2)
        # map generation
        xi = np.linspace(minimum, maximum, contour_steps);
        yi = np.linspace(minimum, maximum, contour_steps);
        zi = griddata((lx, ly), llink, (xi[None,:], yi[:,None]), method='linear')
        ax.contourf(xi, yi, zi, cmap="RdYlGn", vmin=0, vmax=1)
        plt.savefig('heatmap.png', transparent=True)


    Read more »

  • Speech recognition in ROS with PocketSphinx

    Humpelstilzchen12/08/2018 at 18:12 0 comments

    Current goal is to issue voice commands to the Wild Thumper robot. The speech recognition engine PocketSphinx was chosen for this task because it works with little CPU and memory. Since there does not seem to be an up to date ROS node for Pocketsphinx I decided to write a simple one. Pocketsphinx includes a GStreamer element, so the modular GStreamer Framework can help with the audio processing.

    In GStreamer complex tasks like playing a multimedia file is performed by chaining multiple elements to a pipeline. Each element executes a single task, e.g. read a file, decompress data or output data to a monitor.

    Pocketsphinx requires the audio to be 16 bit little endian mono at a rate of 16000Hz. The ALSA plughw interface can provide the input from the USB microphone in this format, so "plughw:1,0" is used as input device.

    Pocketsphinx is used in two voice recognition modes:

    1. Keyword detection

    The speech recognition should only react to commands when addressed by name, for example "wild thumper stop", not just "stop" because the robot should not react when e.g. a movie is running in the background where someone says "stop". Also the robot shall only react to its exact name, not something sounding similar. Pocketsphinx provides a keyword spotting mode for this use case. Input to this mode is  the file keywords.kws with a threshold for each keyword:

    wild thumper /1e-11/

    2. Fixed grammar

    After spotting the keyword, Pocketsphinx shall recognize a command like "stop", "go forward one meter", "backward", "turn left" or "get voltage". Pocketsphinx is run with a given grammar in the Java Speech Grammar Format (JSGF) format to avoid a spoken "go forward" accidentally getting recognized as "go four" (yes, this happens a lot). Since "go four" is not allowed in the grammar it is discarded. This increases the recognition accuracy from ~40% to ~80%. As of today the jsgf option of the Pocketsphinx GStreamer element is only supported in unreleased git, so it needs to be compiled from source. The robot.jsgf looks like this:

    #JSGF V1.0;
    grammar robot;
    <bool> = (on | off);
    <number> = minus* (zero | one | two | three | four | five | six | seven | eight | nine | ten | eleven | twelve | thirteen | fourteen | fifteen | sixteen | seventeen | eighteen | nineteen | twenty | thirty | forty | fifty | sixty | seventy | eighty  | ninety | hundred | thousand | million);
    <misc_command> = (light | lights) [<bool>];
    <engine> = (stop | forward | backward | increase speed | decrease speed);
    <get> = get (temp | temperature | light | voltage | current | pressure | mute | mic | silence | speed | velocity | position | angle | compass | motion | secure | engine | odom | humidity);
    <go> = go (forward | backward) <number>+ (meter | meters | centimeter | centimeters);
    <turn> = turn (left | right | (to | by) <number>+ [(degree | degrees)]);
    <speed> = set+ speed <number>+ | set default speed;
    public <rules> = <misc_command> | <engine> | <get> | <go> | <turn> | <speed>;

    As acoustic model the default U.S. English continuous model of Pocketsphinx is used together with a MLLR adaption for my specific accent and microphone. According to tests with this improved the recognition accuracy to over 90%.

    Pocketsphinx ROS node

    The GStreamer pipeline in the ROS node uses two Pocketsphinx elements, one for the keyword spotting mode, one for the JSGF grammar mode. A preceding "cutter" element suppresses low background noise. The valve before the JSGF grammar node...

    Read more »

  • serving beverages etc

    Humpelstilzchen10/28/2018 at 08:26 4 comments

    The robot can also serve beverages :)

    For details see comments:

    In the following video the robot was driving on manual with a linear speed of 20cm/s and an angular speed of 0.5 rad/s.

    Possible enhancements include:

    - Add weight sensors to measure the load

    - Add an accelerometer to the serving plate and linear actuators to its leg to keep the load horizontal.

  • Honoring limits

    Humpelstilzchen10/20/2018 at 12:09 0 comments

    When the Wild Thumper Robot is driving around my home there are some areas where it shouldn't go because of e.g. cables lying around which are too thin for the sensors to spot. Same for my Neato Botvac robot vacuum. The Neato people solved that by the use of a magnetic strip. The vacuum is not allowed to driver over it. I want my Wild Thumper to honor the same limits. Problem is the relative high distance of 6-7cm between the sensor position and the ground:

    The distance is too high for usual magnetic sensors like reed switches or Hall effect sensors, but not high enough for a magnetometer since they need to be sensitive enough to measure the earth magnetic field for their use as compass sensors. Luckily I still had an old CMPS10 IMU lying around which does the job perfectly. From now on the Wild Thumper can stop at the same areas as the vacuum cleaner.

  • Checking I2C pull-ups

    Humpelstilzchen10/03/2018 at 08:06 0 comments

    TL;DR: Always check your signals with an oscilloscope!

    Long version:

    I wanted to try out rosruby, a ROS wrapper for the ruby programming language, so I decided to port the ROS node for the LPD8806 LED strip of this robot to it. This strip uses a somewhat crippled SPI with only MOSI and SCK to control the lights. While testing the new node the I2C bus immediately and repeatable fails. Since I2C is used as the connection to e.g. the motor controller, the bus is critical for operation. This has not happened before. After ruling out power issues this smelled like a problem with electromagnetic interference, which is why I checked the SPI signals (yellow: SCK, green: MOSI) with an oscilloscope:

    While there are some spikes in the SPI signal and I'm certainly missing the line impedance matching resistors for termination of the bus I don't consider the signal bad enough to disrupt a healthy I2C.

    So I looked at the I2C bus instead which resulted in the following horrific picture (yellow: SCL, green: SDA)

    This by any means is matching more a saw tooth then a clean rectangular signal. It is obvious that the value of 5k Ohm for the pull-up is way too high in this robot. After replacing both resistors with 1k ones the signal is now much better (yellow: SCL, green: SDA):

    Also the I2C no longer fails when sending data on the LPD8806 SPI.

  • Follow me - part 4

    Humpelstilzchen09/09/2018 at 07:58 0 comments

    In the last attempt to make my robot follow me, I used a radio frequency of 433MHz with a small bandwidth of 90kHz. The major problem with this approach were reflections resulting in multipath interference. A technology immune against this issue is ultra-wideband (UWB), which is using a large bandwidth with more then 500MHz. The DWM1000 Module by Decawave is one implementation of UWB, quote:

    "DWM1000 is an IEEE802.15.4-2011 UWB compliant wireless transceiver module based on DecaWave's DW1000 IC.  DWM1000 enables the location of objects in real time location systems (RTLS) to a precision of 10 cm indoors, high data rate communications, up to 6.8 Mb/s, and has excellent communications range of up to 300 m thanks to coherent receiver techniques."

    Originally designed for 2d/3d localization, the DWM1000 modules basically enable the measuring of the distance between a "tag" and an "anchor". To test the DWM1000 I bought three Localino v1.3 boards. One board includes a DWM1000 and an AVR Atmega328 where I installed an open source dw1000 library on. Two DWM1000 are mounted on my Wild Thumper robot, one on each side between the front and the aft wheel about 29cm apart.

    Both DWM1000 on the robot are configured as anchors. The third DWM1000 is configured as a tag and is the beacon that is to be followed:

    Given the two distances dist_left, dist_right from both anchors to the tag and the distance between both anchors dist_left_right we get two right-angled triangles that share a side. With them the beacon position (x, y) from the center of the robot can be calculated using the Pythagorean theorem:

    There are two solutions for these equations, one in front of the robot, one behind. Since the beacon is initially set up to be in front of the robot and is expected to stay there, only this one solution is taken. The calculated relative position of the beacon is then smoothed with a simple Kalman filter [1]. The prediction of the Kalman filter assumes a stationary object and is only adjusted when the robot itself moves. The prediction is corrected by the calculated x & y values from the equations above. The working code can be seen in the following video where the Wild Thumper robot follows a DWM1000 beacon which is mounted on a R/C car.

    [1] The simple Kalman filter is based on the Matlab code in Xilinx Xcell Journal Issue 53, p. 74.

  • Light demo

    Humpelstilzchen08/12/2018 at 07:11 0 comments
  • Follow me - part 3

    Humpelstilzchen06/18/2018 at 10:55 0 comments

      Since using a beam antenna wasn't as accurate as I hoped for direction finding (see previous log) I was looking for other alternatives, so I tried using the phase difference between two antennas, sometimes named tdoa. This method basically uses the doppler effect, but instead of rotating the antenna physically, the antenna is rotated virtually, by switching between one or more antennas. The idea is 

      1. when one antenna is closer to the transmitter, there is an audible glitch in the signal
      2. when the transmitter is equidistant to the antennas, there is no change in the signal

      My phase difference receiver is using two antennas. Switching between two Lambda/2 antennas is done using RF Pin Diodes:

      The diodes are driven with a PWM from an AVR Attiny. Using a capacitor the amplitude of the PWM is shifted to -2,5/+2.5V. The HF is connected to an rtl sdr, decoding is done with GNU Radio. The whole setup looks like this:

      The transmitter has to be FM, the closest thing to that in my inventory was an RFM12 (433MHz), which does Frequency Shift Keying (FSK). I programmed the RFM12 to send a few zeroes so I can easily identify the signal. The result looks like this when the left antenna is closer:

      As one can see I use a PWM with a ratio of 30% : 70% to distinguish between both phases. When the right antenna is closer the spikes are inverted. When both antennas are equidistant distance from the transmitter the signal is not disturbed:

      So the setup basically works, the robot can distinguish if the transmitter is closer to the left, closer to the right or equidistant to both antennas. But there are still a lot of problems, mostly from reflections which do make the robot drive to the wrong direction...

  • Updating the AVRs over I2C

    Humpelstilzchen01/19/2018 at 07:12 0 comments

    The wild thumper has an AVR Atmega32 and an Atmega328 microcontroller. Normally to flash a new firmware onto them I have to open the upper panel and attach my ISP. To workaround that I developed an I2C bootloader. After setting the fuse bits "Boot Reset vector Enabled" and "Boot flash section size"=1024 words the bootloader is programmed into the AVR.

    On start the bootloader checks if the first byte in the flash is 0xff. If it is 0xff it assumes that the memory is erased and there is no valid program. Else it starts the main program by jumping to address 0x00. There is also the option to force the bootloader to run by writing the magic number 123 into the first address of the eeprom. When the bootloader runs it accepts commands from I2C (defaults to address 0x50/0x28). Commands include

    • read memory
    • write memory
    • erase memory
    • erase all
    • and jump to address

    I also wrote the corresponding client program which runs on the Linux computer. With this python script programming an AVR becomes to

    ./ 0x52 main.hex -j

     First argument is the target address 0x52 (0x29), second argument is the program file to flash, -j tells the bootloader to start (jump to) program after flashing.

    From now on I can update the firmware of all microcontrollers in the field without taking everything apart or attaching an extra programmer.

  • Follow me - part 2

    Humpelstilzchen01/14/2018 at 09:46 0 comments

    FInally got a HB9CV antenna, a BNC cable and a corresponding adapter to my RTL-SDR. I've put the antenna onto a servo and 3d printed an adapter, the whole setup looks like this:

    The beacon was quickly wired on a breadboard. The antenna does not have the correct length, but this was enough to receive a signal in a first test:

    On the software side the beacon simply transmits "MO" in Morse code every 5s with currently 50ms for a dit and 150ms for a dah. I have to speed it up later. The first reception was done with multimon-ng. Minimum frequency of the transmitter is 433.82MHz, maximum is 434.02MHz, so I used the center frequency of 433.92MHz with a bandwidth of 200kHz. Gain was set to maximum and everything piped to multimon-ng:

    rtl_fm -g 19 -f 433.92e6 -M am -s 200e3 -r 22050 - | multimon-ng -a MORSE_CW -t raw /dev/stdin

    After verifying that multimon-ng prints "MO" every 5s I wrote a small python script to decode the same output of rtl_fm. The data is sampled 45 times a second as short int:

    frames =*2) # Two bytes per value
    data = np.array(struct.unpack("<%dh" % (len(frames)/2), frames))

     and then the average is used to detect if this is a high or low level:

    avg = np.average(data)
    cur_level = avg > HIGH_THRES

    With help of the Internet decoding the morse code was very. In the end every time I detected a "MO" I printed the average high level on the terminal.

    Using the average level I placed the beacon every 30° around the antenna with a distance of 1.5m to a get a radiation pattern of the antenna. My setup was not very accurate but it does give a first idea:

    As one can see the maximum is not quite in front of the antenna, it does squint about 30°. But the decrease of values to 0° and 60° degree is clearly visible. The exact values in this area need more investigation.

View all 17 project logs

Enjoy this project?



Rud Merriam wrote 08/06/2018 at 16:40 point

Would like to see the code for this. Can you share it, please?

  Are you sure? yes | no

Humpelstilzchen wrote 08/06/2018 at 16:57 point

The project is based on the Robot Operating System, so most code comes from there, everything else (e.g. the ROS configuration files, the AVR code) is in my git repository

  Are you sure? yes | no

Rud Merriam wrote 08/06/2018 at 18:52 point

How can I clone it from that URL?

  Are you sure? yes | no

Humpelstilzchen wrote 08/06/2018 at 19:18 point

  Are you sure? yes | no

Tegwyn☠Twmffat wrote 07/03/2018 at 08:27 point

Have you thought of burying RFID tags under your carpet for indoor navigation? I wonder if it would work?

  Are you sure? yes | no

Humpelstilzchen wrote 07/04/2018 at 07:30 point

Havn't thought of that yet, I'm using SLAM for indoor navigation. Also I'm currently testing DWM1000 modules.

  Are you sure? yes | no

Tegwyn☠Twmffat wrote 07/04/2018 at 08:12 point

Searching with google I only find one instance of it being used:

  Are you sure? yes | no

evansgp wrote 07/01/2018 at 05:45 point

Hi! Your project is awesome and I'm inspired to do something similar as my next project. One question: I was looking at the Wild Thumper chassis and it says the motors that have built in encoders aren't supported. Which ones are you using? Did you have to do anything special for them to fit?

Edit: Oh, I think I found an answer to my own question:

  Are you sure? yes | no

Humpelstilzchen wrote 07/04/2018 at 07:28 point

Thanks and the pdf is correct, you will need an  additional hole. Btw if you get the 6wd base you will only need encoders for the 2 center motors.

  Are you sure? yes | no

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates