Add a custom sensor to a robot kit
·
Angie Mercurio

We received a note on Instagram that printables.com was holding a challenge to create a playful design for the Otto robot kit from HP Robots. I thought this would be a great opportunity to demonstrate some mechatronics skills, and show how to use the nLab to design, build, and test a custom circuit for a robot. Here is my submission!

The Otto kit is a cool concept from Moravia Education. It comes with a main printed circuit board containing an ESP32-WROOM microcontroller, and connections for continuous drive motors, line following sensors, a ultrasonic rangefinder, and an LED ring. It can be programmed with an online app, or with MicroPython. It also comes with a rechargeable Lipo battery, USB cable, and some O-rings and a screw driver, but that is it, no robot body! The idea is that you design and 3d print your own robot, and then program it to do a task. I love this idea, because it takes away one of the three difficulties when making a little mobile robot: you need to come up with electronics, mechanical parts, and code. Doing all three is difficult, especially as a beginner, so providing the electronics is a huge help. But I wanted to demonstrate that you aren't limited to using just the parts that come with the kit, so I designed a custom sensor to add on to my robot.

I decided to give Otto the ability to hear sounds, and, based on two microphones, be able to turn towards the direction of the sound. The theory is that sound actually travels relatively slowly, about 343 m/s, so if there is a sound off to the left of Otto, it will hit the left microphone a few hundred microseconds before the right microphone. So, if I read the voltage from two microphones really fast, I can tell which one hears an event first, and turn in that direction.

The further apart the microphones, the bigger the delay in signal between the left and right microphones, and the easier it will be to detect the difference. I started by testing a circuit to verify that this would be possible.

Here is a typical microphone circuit with a high-pass filter and amplifier:

The output voltage is half the power supply voltage, and varies up and down from there, with a larger for louder sounds. I built two of these, and used a dog training clicker to make a sharp sound:

Here you can see that the left microphone, in green, hears the click before the right microphone, in yellow, by a little more than a millisecond. I used my nLab to power these circuits and visualize the signals, with the trigger function to freeze the data when the sound occurred. This demonstrates the usefulness of a tool like nLab: a breadboardable oscilloscope, power supply, and function generator. It allowed me to test my design, independently from the Otto, without worrying about how to power the circuit or code the visualization from scratch.

The next task was to make sure the ESP32 on the Otto could read to voltages fast enough to identify when one voltage changed before the other. I decided to install a generic version of MicroPython for the ESP32-WROOM on Otto, so that I could program it with the Thonny IDE. That meant using PIP to install the esptool from ESPRESSIF on my computer to be able to flash MicroPython onto Otto. 

I wrote some sample code to test the ability of the ESP32 to read from two ADC pins very quickly. Based on the tiny differences in time between the two mic circuits, I decided I better try to read the voltages 100,000 times per second, and store the data in arrays. When the signal changes, I print the data back to see if the left or right mic saw the signal change first. This code seemed to prove the ESP capable of reading the ADC pin fast enough for my purpose, I don't think it is actually reading 100k times per second, more likely in the 3-8k range.

import machine, time
from machine import Timer
from time import sleep                     
from machine import Pin, ADC, PWM 
import array
# ADC pins from connectors 6 and 7 on the Otto
analogLeft=ADC(Pin(32))
analogRight=ADC(Pin(33)) 
# set up the data structures
samplerate=100000 
length = 900 
bufL = array.array('H', (0 for _ in range(length)))
bufR = array.array('H', (0 for _ in range(length)))
bufLearly = array.array('H', (0 for _ in range(100)))
bufRearly = array.array('H', (0 for _ in range(100)))
ind = 0 
takedata = 0
savedata = 0
# calculate the average of the mic
c = 0
avg = 0
for c in range(100):
    left=analogLeft.read_u16()
    avg = avg + left
avg = avg / 100
cal = avg
abovecal = 4000
print("cal = "+ str(cal))
# in this timer, read the mics
# continuosly save the data into a LIFO
# if a big pulse is detected, save for a while
def mycallback1(t): 
    global bufL,bufR,bufLearly,bufRearly, ind, takedata, savedata, cal, abovecal
    if takedata == 1:
        left=analogLeft.read_u16()
        right=analogRight.read_u16()
        if savedata == 0:
            if left > cal+abovecal or left < cal-abovecal:
                savedata = 1
            else:
                bufLearly[:-1] = bufLearly[1:]
                bufLearly[-1] = left
                bufRearly[:-1] = bufRearly[1:]
                bufRearly[-1] = right
        if savedata == 1:
            bufL[ind] = left
            bufR[ind] = right
            ind = ind + 1
            if ind == length:
                ind = 0
                takedata = 0
                savedata = 0
# start the timer
tim = Timer(0) 
tim.init(mode=Timer.PERIODIC, freq=samplerate, callback=mycallback1)

i=0
while True:
    takedata = 0
    rx = input() # wait for the user to request data
    takedata = 1
    while(takedata == 1):
        pass # wait until the pulse was found and buffers are full
    # print out the data
    for i in range(100):
        print("["+str(i-100)+","+str(bufLearly[i])+","+str(bufRearly[i])+"],")
    for i in range(length):
        print("["+str(i)+","+str(bufL[i])+","+str(bufR[i])+"],")

Convinced that this should work, I used onshape to design a few mechanical parts for Otto. First, I made some wheel hubs to connect to the drive motors and hold the O-rings as tires. As usual, it took two tries to get the tolerances right, my first hub was a little too small. On the second version I forced the O-ring to stretch just a little to get on the hub, and that compression force keeps the O-ring from falling off.

 

Then I designed a robot chassis to hold the motors, ESP32, battery, breadboard, and microphones. Again it took two tries to get right, on the first version I miss-read my mechanical calipers and made the gap for the motors too small by 0.1 inch. But iteration is the key to success, and this is why prototyping with a 3d printer is so powerful, it didn't take long for me to see the error, correct the CAD, and print again. And each iteration gets a little better, so iterating as many times as possible is key to a successful project!

I made two design decisions here: a raised platform to hold a breadboard above the ESP32 board and battery, and a plastic spoon for a caster. With more time I could manufacture the circuit as a PCB, but for prototyping purposes I kept the breadboard. I used a diamond hole pattern to minimize the amount of material to print, I'm not sure if that actually saves any time when printing. The design is not very attractive, but very practical.

I love using spoons as casters in little mobile robots like this! A traditional caster, like you would find on a chair, or a ball caster, work well with a heavy load and a clean floor, but are difficult to implement on a small robot. I find a disposable plastic spoon works much better in this case!

 

I used some vegetable skewers to hold the microphones as far away from each other as I could, with some small 3d printed adapters to mount the microphones to the skewers. I hot-glued the parts together. This is all about rapid prototyping, no shame in using hot glue! With a few more iterations I would 3d print the entire robot, and use split clamps to hold the parts together, but that will have to be a project for another day.

 

I cut up the cables that came with the line sensors to connect the ESP32 board to my breadboard, and soldered on header pins to make a little plug. I covered the joint in hot glue for strain support. Soldered connections are brittle, and having built a lot of these types of little mobile robots, I know they are one of the first parts to fail. A little hot glue goes a long way in keeping things together for testing.

Finally, a working circuit and a mechanical robot! The last part is the code to control the motors from the sensor data. First, I wrote some sample code to test the motors. The drive motors are continuous RC servos, so the PWM signal has specific requirements to set the velocity and direction of the motors, typically 50Hz with a duty cycle between 2.5 and 12.5%, and 7.5% setting the velocity to 0, and above and below that increasing the speed in either direction. I did some guess and check to find the values for these motors.

from time import sleep         
from machine import Pin, PWM

pwmL = PWM(Pin(13))
pwmR = PWM(Pin(14))
           
pwmL.freq(50)
pwmR.freq(50)

# off between 45 and 55
# less than 50 is clockwise
# more than 50 is counter clockwise
def getDuty(speed):
    duty = (0.025+speed/100*0.1)*65535
    return int(duty)
def straight(t):
    pwmL.duty_u16(getDuty(30))
    pwmR.duty_u16(getDuty(70))
    sleep(t)
def off(t):
    pwmL.duty_u16(getDuty(50))
    pwmR.duty_u16(getDuty(50))
    sleep(t)
def left(t):
    pwmL.duty_u16(getDuty(30))
    pwmR.duty_u16(getDuty(30))
    sleep(t)
def right(t):
    pwmL.duty_u16(getDuty(70))
    pwmR.duty_u16(getDuty(70))
    sleep(t)

while 1:   
    #print("straight")
    straight(1)
    #print("off")
    off(1)
    #print("right")
    right(2)
    off(1)
    #print("straight")
    straight(1)
    off(1)
    #print("left")
    left(1.8)
    #print("right")
    off(1)

Then I wrote some code to read the ADC pins, and print out which one heard the sound first. I decided to keep things simple, and have Otto take a little step in the direction of the mic that heard the sound first, instead of trying to calculate the angle and turn the exact amount. Some future work would be to use a different algorithm, like an autocorrelation, and add encoders to the wheels, to get exact positioning.

import machine, time
from machine import Timer
from time import sleep                     
from machine import Pin, ADC, PWM 
import array
# motor pins on connectors 10 and 11 on the Otto
pwmL = PWM(Pin(13))
pwmR = PWM(Pin(14))
pwmL.freq(50)
pwmR.freq(50)
# ADC pins from connectors 6 and 7 on the Otto
analogLeft=ADC(Pin(32))
analogRight=ADC(Pin(33)) 
# set up the data structures
samplerate=100000 
length = 900 
bufL = array.array('H', (0 for _ in range(length)))
bufR = array.array('H', (0 for _ in range(length)))
ind = 0 
takedata = 0
found = 0
# calculate the average of the mic
c = 0
avg = 0
for c in range(100):
    left=analogLeft.read_u16()
    avg = avg + left
avg = avg / 100
cal = avg
abovecal = 4000
print("cal = "+ str(cal))
# in this timer, read the mics
# if a big pulse is detected, determine which mic saw it first
def mycallback1(t): 
    global bufL,bufR,bufLearly,bufRearly, ind, takedata, savedata, cal, abovecal, found
    if takedata == 1:
        left=analogLeft.read_u16()
        right=analogRight.read_u16()
        if found == 0:
            if left > cal+abovecal or left < cal-abovecal:
                found = 1
            if right > cal+abovecal or right < cal-abovecal:
                found = 2
        bufL[ind] = left
        bufR[ind] = right
        ind = ind + 1
        if ind == length:
            ind = 0
            takedata = 0
# start the timer
tim = Timer(0) 
tim.init(mode=Timer.PERIODIC, freq=samplerate, callback=mycallback1)
# functions for turning on the motors
def getDuty(speed):
    duty = (0.025+speed/100*0.1)*65535
    return int(duty)
def straight(t):
    pwmL.duty_u16(getDuty(30))
    pwmR.duty_u16(getDuty(70))
    sleep(t)
def off(t):
    pwmL.duty_u16(getDuty(50))
    pwmR.duty_u16(getDuty(50))
    sleep(t)
def left(t):
    pwmL.duty_u16(getDuty(30))
    pwmR.duty_u16(getDuty(30))
    sleep(t)
def right(t):
    pwmL.duty_u16(getDuty(70))
    pwmR.duty_u16(getDuty(70))
    sleep(t)
# start off
off(1)
while True:
    found = 0
    takedata = 1
    while(takedata == 1):
        pass # wait until the pulse was found and buffers are full
    print(found)
    if (found == 1): # left saw it first
        left(.6)
        straight(1)
        off(0.1)
    if (found == 2): # right saw it first
        right(.6)
        straight(1)
        off(0.1)

Overall, this was a fun project! As a professor, I see a lot of learning opportunities for a kit like this. Everyone could be given the same CAD files and the challenge could be to write the best code to do a task, like line following, or give the same code and have everyone build different mechanical parts. Or open up the challenge to CAD and coding, but expect the project to take a lot of time, it will take many iterations of the 3d printing and programming to get things right. The key is to break up the task into smaller, debuggable parts, and test as you go. 

Thank you to Printables and HP Robots for sponsoring this contest, and providing a 30% off discount on the Otto starter kit.