Projects

AI-Powered Dog Trainer Part 2

with servo-powered pet treat dispenser

Liam Davies

Issue 50, September 2021

This article includes additional downloadable resources.
Please log in to access.

Log in

Expanding on our AI-Powered Dog Trainer with additional functionality, 3D printed enclosure, and a servo-powered pet treat dispenser.

BUILD TIME: an afternoon (+3d printing time)
DIFFICULTY RATING: Intermediate

Last month, we showed how to set up your own live audio classifier with TensorFlow on a Raspberry Pi Zero. This month, we’re finishing off the project by completing the Python program, 3D printing a fancy enclosure and even building a pet treat dispenser!

3D Printing

We’re jumping straight into it this month by 3D printing a new set of parts that help make this project appear more like a household gadget than a mess of wires. These parts were all printed on a small Flashforge Finder, and while they won’t win any awards for design, they do the job of tidying the project up. Only one wire is visible at the end connecting the main processing unit to the treat dispenser.

Main Enclosure

The main enclosure will be responsible for holding the Raspberry Pi Zero W, the Blue Yeti microphone, the small speaker, and the USB powerbank. The design of this can very much be customised to suit the application, considering it could be connected to a wide variety of devices and have it work just as well.

We settled for a two-part enclosure. The front half holds the Blue Yeti microphone with a two-point attachment screw system, with the same spacing as the original microphone holder. The cylindrical cut out on the bottom allows the very bottom of the microphone to be exposed for connection of its mini-USB port. The front of the enclosure has a speaker grille and allows the 3W speaker to be bolted to it, however, we later discovered the enclosure is too narrow for the speaker and the design ends up physically bending it. It doesn’t seem to modify the sound in any major way.

The rear enclosure is a much smaller assembly, and its main purpose is to securely mount the Pi Zero and act as a holder for the powerbank. The powerbank used for this project is a 10,000mAh Cygnett unit, and has plenty of capacity for keeping the dog trainer running for quite a few hours. However, the enclosure also has sufficient room to simply run a USB power cable out the back to power the system off a mains power adapter. The cables can be routed through the rectangular hole beneath the Pi Zero and out the rear of the unit.

Each half of the enclosure includes four aligned 8mm holes for neodymium magnets. These magnets are surprisingly effective. Compared to M3 nuts and bolts we usually use, these magnets snap together without messing about with screwing in nuts and drilling shafts. The halves won’t come apart during regular use, but pulling them apart can be done in seconds and modifications can be made without hassle.

There are some notes to be made about using these strong magnets, though. Gluing them should be done carefully because it’s quite easy to put them in backwards and cause corresponding magnets to repel instead of attract. Using a Sharpie marker, I made a little mark on the opposing faces that should be glued to the plastic, and with a little bit of superglue, they’re in and staying in.

It gets pretty packed inside the enclosure, so shortening of cables may be needed.

The Dispenser

As we discussed last month, we need a way to reward our dog, Biscuit, for good behaviour. So, we opted to design a treat dispenser that can be triggered by the Raspberry Pi Zero.

Having seen a variety of dispensers for small items on YouTube, we were careful about what to spend our time on when it came to designing the model. Often, designs online jammed in a lot or didn’t output a reliable number of items. A common method was to use a hopper with a small rotating element at the nozzle, however, if the object was too big or oddly shaped, it has a chance of jamming in the end.

For our second approach, we designed a sort of radial funnel, with the highest point in the middle and a flat section around the outside. As the cone rotates, more treats fall down onto the flat section and slowly get pushed into the hole outside of the disc. The idea here was to prevent jamming like a standard hopper design. Since it only allows one layer of treats into the output chute at once and pushes the others away, it would hopefully avoid the jamming problem.

However, we felt this somewhat overcomplicated the design and it could be subject to other problems besides jamming, such as a giant pile of treats falling down at once rather than one at a time (Kind of like the coin pusher arcade machines!) Additionally, we want these models to be adaptable to any item, and this model has been specifically made to suit these small dog treats.

Following the KISS (Keep It Simple, Stupid!) principle, we eventually settled for a turntable design that simply has treat slots around the outside. Once it rotates far enough, the treats fall through a chute and get dispensed. While it doesn’t have a very high capacity, it should be fairly reliable.

This FS90R servo is exactly the same as the one we used in the Arduino-powered Vending machine project so we know it works quite well, and it would have the torque to help move the turntable around. This is a continuous servo so it can move an unlimited number of times, as opposed to a pre-determined range of rotation.

We were initially shying away from using a gear system for this project as 3D-printed gears can sometimes be fiddly, need high print resolution and often lubrication as well, but it eventually turned out to be the most effective option.

First of all, we’re using a gear reduction of 17:4, with 85 teeth on the turntable gear and 20 teeth on the servo gear. This has the benefit of both a higher torque and lower speed, decreasing the possibility of jamming and increasing the accuracy of the servo. If the servo reads a rogue PWM signal or moves on start-up, less unwanted behaviour will result since the wheel moves only a quarter as far as the servo itself.

The other benefit of using a geared system is that we can mount the turntable on a single ball bearing that allows it to spin freely around the holder base, as opposed to directly on top of the servo. There is some friction to the imperfect finish of the plastic, but it isn’t too much of an issue because of the torque advantage of the gear system.

The servo mounting system is adjustable, with two small rails that can move in and out. It needs to be positioned in far enough that it doesn’t jam the wheel moving around but doesn’t skip teeth either. The included servo screws can mount it directly to the plastic.

The Build:

Dispenser Circuit

Parts Required:JaycarAltronicsCore Electronics
1 x LM393 Voltage ComparatorZL3393Z2558-
1 x 10kΩ Resistor *RR0596R0582SS110990043
1 x 1kΩ Resistor *RR0572R0558SS110990043
2 x JST XH Connectors (2-way) *PT4457-ADA1131
1 x Grove 4 Pin Connector--SS110990030
1 x 8-pin DIP IC SocketPI6452P0550-
1 x Bakelite Universal PlatesHP9542H0714ADA2670
1 x Pimoroni Audio Amp SHIM---
1 x Raspberry Pi Zero W (Wireless)-Z6303AADA2816^
1 x Mini HDMI to Standard HDMI Jack AdapterPA3645Included AboveADA2819
1 x Micro USB OTG Host CableWC7725Included AboveADA1099
1 x Speaker - 3" Diameter - 4 Ohm 3 Watt--ADA1314
1 x microSD Memory Card - 16GBXC4989DA0365DF-FIT0394
1 x USB Microphone %AM4136D0985ADA3367
1 x FS90R Continous Servo-Pololu.com: 2820-
1 x 8mm Ball Bearing--MB87311
1 x 10kΩ Horizontal TrimpotRT4360R2480BADA356
1 x IR Emitter And Detector Pack-Sparkfun.com: SEN-0024-
10,000mAh Cygnett Power Bank #

Parts Required:

1 x LM393 Voltage ComparatorZL3393
1 x 10kΩ Resistor *RR0596
1 x 1kΩ Resistor *RR0572
2 x JST XH Connectors (2-way) *PT4457
1 x Grove 4 Pin Connector-
1 x 8-pin DIP IC SocketPI6452
1 x Bakelite Universal PlatesHP9542
1 x Pimoroni Audio Amp SHIM-
1 x Raspberry Pi Zero W (Wireless)-
1 x Mini HDMI to Standard HDMI Jack AdapterPA3645
1 x Micro USB OTG Host CableWC7725
1 x Speaker - 3" Diameter - 4 Ohm 3 Watt-
1 x microSD Memory Card - 16GBXC4989
1 x USB Microphone %AM4136
1 x FS90R Continous Servo-
1 x 8mm Ball Bearing-
1 x 10kΩ Horizontal TrimpotRT4360
1 x IR Emitter And Detector Pack-

* Quantity Required, Sold in packs

% Any USB microphone will work. We used a Blue Yeti studio microphone.

^ Sold as a Starter Pack

# Use whichever powerbank you have on hand.

To help reduce the number of wires running out from the back of the dispenser, we’ve opted to put together a small circuit on a prototyping board, both to manage the power connections and generate the trigger signals for the treats. In hindsight, this board is probably too small to be soldered conveniently, but it does do the job and keeps it all compact.

When we used the continuous servos with the Vending Machine project a couple of months back, one of the biggest issues we found was that the controller wasn’t aware of whether a lolly fell out or not. So, it had to guess how far the servos had to move. This open-loop control system was quite flawed as it wasn’t uncommon for no lolly to fall out and the customer was charged 20c anyway. Bad business!

This time, we have designed a feedback system that can move the treat turntable in small incremental movements and immediately detect when a treat does fall out. We’re using IR detectors and emitters that emit directional beams of light. When broken, even for a split second, we know that a treat has been dispensed. These two IR units will be fixed into two rectangular grooves inside the dispensing hole.

IR Headers

We first soldered in two JST XH connectors for the IR emitter and IR detector. These connectors are very common in 3D printers and Arduino modules. They’re non-reversible so it’s impossible to connect them with the incorrect polarity.

We also added a four-pin Grove connector, which unfortunately doesn’t have 0.1” (2.54mm) spacing for prototyping and breadboard compatibility, so a little bit of bending was required. Four pins is all we need for this though, two for 5V and Ground, and two for servo signal and IR detector signal.

The left-most JST connector is used for the IR emitter and acts as a simple LED, so a resistor is required to regulate the current. Many people forget that infrared LEDs have very low forward voltages of around 1.2V, so it’s easy to burn them out by using a resistor suitable for regular visible-light LEDs. In experimentation, we found just a couple of milliamps flowing through the emitter was enough for the detector to pick it up. We settled with a 1kΩ resistor between ground and the negative side of the JST connector for this purpose to keep current draw low.

The IR detector, on the other hand, is a bit more of a complicated circuit. By hooking the IR detector in series with a 10kΩ resistor and sampling the point between them, a simple voltage-divider circuit can be made that varies its voltage based on whether the detector can ‘see’ the emitter. You’ll also need to power it with the red 5V wire. However, to help keep detection of the treat falling out reliable, a comparator circuit with an adjustable sensitivity will be used. Its function is simple; is the signal of the IR detector higher than a threshold? If so, we output a digital signal the Raspberry Pi can use to detect when a treat falls out. The signal will be configured as an interrupt in the Python code, so we don’t need to continually poll the pin. An interrupt essentially functions like receiving an email notification on your phone as opposed to continually checking your inbox every other minute (i.e. polling).

It’s worth noting there are some problems with this circuit if being used with devices that can’t be programmed to handle the digital signals in a smart way. For example, if your microcontroller doesn’t have interrupt pins, it may have a hard time polling fast enough to detect that a treat has fallen past the sensors. Additionally, multiple interrupts are often fired due to a lack of hysteresis on the voltage comparator. (i.e. at the switching point, there is enough noise for the comparator to turn on and off rapidly). If your controller can’t reject further interrupts after the first one, you may have to implement additional feedback resistors to ensure the voltage comparator only switches once. Since we’re only waiting for one interrupt in the code and ignoring the rest, it’s not an issue.

We purposely didn’t use a 5V pullup resistor on the voltage comparator output. The Pi Zero’s outputs are NOT 5V tolerant, so while it can drive 5V devices like the servo motor, it cannot safely read higher voltages than 3.3V. Since the comparator has an open-collector output, if we enable the Raspberry Pi’s internal 3.3V pullups, the comparator can pull it down to ground when turned on.

Next, we can solder in the socket for the LM393 IC. This is just a simple 8-pin socket that allows us to swap it out with a different comparator later if desired. The LM393 is a dual-comparator package, however, we’ll only be using one. First, the signal from the IR detector can be soldered to the inverting input of the comparator.

For adjustment of the sensitivity threshold, a 10kΩ horizontal trimpot was used. The outer legs can be connected to 5V and Ground to make a simple voltage divider. The middle pin is connected to the non-inverting input of the LM393.

The top left-pin of the comparator is then connected to the white (3rd) pin of the grove connector, which is the digital output of the detector signal.

To control the servo, we also added a three-long header bank. The left-most wire is soldered to the yellow signal grove wire and the other two are 5V and Ground power respectively. After we fixed a ground issue with the IR detector (see the long black wire in the photo below), it was ready to be hooked up to our external components!

While the servo already has factory header pins, we need to solder some JST plugs to the IR detector and emitter. We used some heatshrink and made the leads quite short to avoid any cable messes in the dispenser.

The sensors can then be popped into the enclosure with some double-sided tape and aligned so they face each other.

Now, it’s time to pop all the connectors into the board and connect the grove wire to the Raspberry Pi Zero W. Some splicing and resoldering may be required for the grove connector to add female jumper wires to them.

A couple of words of warning about the GPIO headers of the Raspberry Pi Zero; they are NOT labelled! This means it’s very easy to mess up and short things that aren’t meant to be shorted. The 3.3V rail, for example, can only supply a very limited supply of current. We want to be using the 5V rail as it’s directly connected to the 5V output of our powerbank, whatever current the Raspberry Pi uses. In testing, about 120mA-130mA is used for the Raspberry Pi Zero (awesome considering it’s an entire computer) and another 200-300mA is used when running the servo. Doing a quick calculation based on the 10,000mAh powerbank, and assuming we’re using an average of 150mA, the trainer can run for around 65 hours. Awesome!

The Raspberry Pi lineup officially includes just one PWM pin on GPIO18, which will be used for our servo control. Although other pins have been tested and do work, for sake of correctness we’re using the pin meant for generating PWM signals.

Final Assembly

Now we can put everything together! The main enclosure gets a little packed when connecting all the cables for the speaker and microphone but some zip ties later and it’s nice and tidy. We recommend using short cables to reduce the difficulty of compacting everything into the box.

Next up, we need to finish the treat dispenser. We used some thin strips of double-sided tape to secure the small control board onto the bottom of the turntable holder. Some Hook and Loop or zip ties ensure that the cables don’t go everywhere and let the dispenser sit flat.

3mm hex nut and bolts have also been added to the bottom of the dispenser so it can optionally be mounted onto objects or positioned higher to prevent curious dogs fiddling with it.

To fill up the dispenser, we suggest using uniformly sized treats that are about half to 2/3 the width of the turntable holes. We found any larger and they get quickly stuck in the output chute, and any smaller results in poor detection of the treats with the IR sensors. To keep it safe, we pushed on the friction-fit lid and put it on a desktop to test it. As a quick test to ensure the mechanics are working, the centre-point screw on the bottom of the servo can be turned slightly to trick the servo into moving.

The Code

The code of this project will be adapted from Part 1. We’re going to be creating our own Python modules, which will help in keeping our code clean and organised.

We first created a ‘dispenser’ module by simply making an empty text file called ‘dispenser.py’. If you are executing Python code in the same directory, it’s as simple as running ‘import dispenser’ and you then have access to the code provided in the file.

Our main function in the dispenser module is the treat() function which works with the control board we made to pop out one treat. There are a couple of different methods for moving servos when we have feedback systems like the IR detectors, however, our strategy is to move the Servo at a slow to medium speed in small steps until we detect the IR sensor’s line of sight is broken. We’re doing it in small steps as opposed to continually, as we found that moving the turntable at very slow speeds is unreliable and causes intermittent juddering behaviour, so moving it in quick steps is more reliable.

import RPi.GPIO as GPIO
import time as time
 
SERVO_SPEED = 3
TIME_STEP = 0.12
SERVO_PIN = 18
DETECT_PIN = 4
DISPENSE_TRIES = 5
 
GPIO.setmode(GPIO.BCM)
GPIO.setup(SERVO_PIN, GPIO.OUT)
servo = GPIO.PWM(SERVO_PIN,500)
servo.start(0)
 
GPIO.setup(DETECT_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)

You’ll notice all of this code is not contained within a function, so this setup code is run immediately when the module is imported. The variables in constants define how our dispenser should run, and will vary based on the servo used and the GPIO configuration.

The most important to modify, though, is the SERVO_SPEED and TIME_STEP, which define how the servo responds when rotating the turntable. The SERVO_SPEED is expressed as a percentage of a PWM duty cycle, so in our case, the servo speed is generated by a 3% increase in the duty cycle. The TIME_STEP is how long each movement of the servo should last.

The rest of the code is responsible for setting up the GPIO for the IR detector and servo controller. Note that the pullup resistor (PUD_UP) set on the DETECT_PIN is super important! Since we don’t have an external one and the output of our voltage comparator (LM393) acts as an open-collector output, it’s only capable of pulling down an already existing voltage to ground; it can’t actually generate a voltage.

def treat(times=1):
    print(f'Dispensing {times} treats...')
    for i in range(times):
        dispensed = False
        tries = 0
        while not dispensed:
          #First run the servo
          servo.ChangeDutyCycle(50 + 
SERVO_SPEED)
            time.sleep(TIME_STEP)
            servo.ChangeDutyCycle(0)
 
            #Check if the treat fell out
            if GPIO.wait_for_edge(DETECT_PIN,  
GPIO.FALLING, timeout=500) != None:
              print(f'Treat {i+1} dispensed.')
                dispensed = True
                continue
  #Give up if we’ve tried to dispense enough times.
            tries += 1
            if tries >= DISPENSE_TRIES:
              print("Treat dispensing failed.")
              return
     print("Finished.")
def stop():
    servo.stop()
    GPIO.cleanup()

This code actually moves the servo and ensures it doesn’t run forever by ensuring it only tries a limited number of times. The middle point of a continuous servo is actually 50% of our duty cycle (considering we’re operating at 500Hz), so this value represents no movement at all. We add our speed value onto this figure to obtain, in our case, 53% duty cycle.

To stop the servo’s movement, we didn’t change it to 50% but instead to 0%. 50% tends to cause a lot of jitter and slight movements over time, so completely disabling the output prevents this problem. Finally, the stop() function cleans up the GPIO class and prevents any other applications or instances of the program from causing any weird interactions with GPIO.

if __name__ == "__main__":
    #If we're executing this script 
    #as standalone.
    print("[Enter 0 treats to end program.]")
    num = int(input("Input number of 
treats to dispense:"))
    while num > 0:
        treat(num)
        num = int(input("Input 
number of treats to dispense:"))
    stop()

This block of code is completely optional but is useful for testing the dispenser and introducing it to your dog. In Python, it’s possible to detect when the code file is being used as a standalone file, and not imported into a different script. If so, we can run code that takes a user input for the number of treats to dispense, and keeps asking and dispensing until we enter 0.

Moving onto the main file, we have made several modifications changing behaviour over the lasts month’s program. Instead of just a test file, we have made a number of modifications that makes the dog trainer fully featured and integrated with our latest dispenser system. We’re only showing the most important parts of the code, the rest can be found in the project files on our website.

def detect_bark(confidence):
    global time_target
    confidence_perc = confidence * 100
    now = datetime.now()
    current_time = 
now.strftime("%m/%d/%Y %H:%M:%S")
    print(f"[{current_time}] Detected dog 
barking! (Confidence: {confidence_perc:.1f}%)")
    report.reportBark(current_time, 
confidence_perc)
    delta = timedelta(seconds=WAIT_TIME)
    # First we check if we're waiting 
    # for the dog (i.e. we have a time target)
    # and the dog has barked in the meantime.
    if time_target is not None:
        time_target = None
        barked = True
        print("Failed.")
        setCooldown()
  # If we don't, we start waiting for the dog.
    else:
        playSound("ComeSound.wav")
        #Start the time target for how long 
        #the dog has to wait.
        time_target = now + delta
        #Dog hasn't barked yet since waiting.
        barked = False
        print("Now waiting for the dog...")
        print("Setting time target to " + time_target.strftime("%m/%d/%Y %H:%M:%S"))

When a bark is detected, there are several states the dog trainer can be in. Our current functionality ensures that the dog must stay quiet for 10 seconds, and additionally after barking a 60 second cooldown starts where no more treats are dispensed. When a bark is detected, we set a time target that the dog has to wait for using the timedelta command. This is, of course, fully customisable.

If we see we’ve already set a time target within the last ten seconds, we know that the dog has failed waiting for the trainer and printed a “Failed” message to the console.

with AudioDevice(audio_system, 
device=audio_device) as source:
    #Test if the waiting time has finished
    for sample in source:
        source.pause()
        input_data = 
[np.array(sample.spectrum(low_freq=250, 
high_freq=2500), dtype=np.float32)]
        interpreter.set_tensor( 
input_details[0]['index'], input_data)
        interpreter.invoke()
        output_data = 
interpreter.get_tensor(output_details[0]['index'])
        if output_data[0][1] > THRESHOLD:
            detect_bark(output_data[0][1])
        source.resume()
        if time_target and datetime.now() 
>= time_target:
            if cooldown_target and 
datetime.now() >= cooldown_target:
              #If the dog hasn't barked 
              #in the designated time,
              #AND the cooldown has finished.
                dispenser.treat()
                print("Success!")
                #Set the cooldown again.
                setCooldown()
                time_target = None

In our prediction loop code (i.e. code that is constantly called as microphone samples are checked), we are checking if we have met our time target (the quiet period) and the cooldown target (the 60 second wait period). If no barks have been detected in that period, we can finally dispense a treat.

We also made one other module called ‘report’ that simply manages the Google Sheets API using Gspread, which we can expand later to implement more functionality into our spreadsheets.

#report.py
import gspread
import json
gc = gspread.service_account()
sh = gc.open("AI Dog Trainer")
def reportBark(time, confidence):
  sh.sheet1.append_row([time, json.dumps(confidence.item())])

Testing

Finally, we can test our dog trainer with our dog, Biscuit. Unfortunately, Biscuit didn’t seem to warm up to it a whole lot, as she often ignored the system when she was busy barking outside or was more obsessed with her beloved tennis ball. We tried it for most of a day and it either didn’t respond to quieter barks or Biscuit just didn’t care. Maybe next time I should make a tennis ball dispenser instead!

However, I got much better results from my other dog Ruby, who has a soft spot for a treat (or twenty). She’d happily sit at the base of the treat dispenser for about five minutes, and scratch at it thinking it would finally give her one. Unfortunately, since I had trained the AI on Biscuit’s bark, it often had trouble picking up on Ruby’s bark. Still, the fact that the AI can pick up on the differences between two similarly sounding dogs is super impressive, and conversely, with some broader training data, it would be possible to generalise its training to any barking sounds.

With regards to the machine’s performance itself, the Raspberry Pi Zero runs perfectly fine all day outside and it even has some impressive WiFi range for reporting data over Google Sheets! Overall, while not as effective as we’d hoped, it was still a fun project to implement clever audio classification tech with. The machine is adaptable enough to create several different use-cases with its microphone and speaker interfaces.

Where To From Here?

In terms of Audio AI applications, there’s plenty of different uses for a project like this, especially with the dispenser. Next up, we’re going to try to program this system to train local birds to respond to a bell ringing sound or something similar and dispense birdseed with the treat dispenser. While there is a limited capacity within the dispenser, it’s enough to do day-to-day training sessions.

Besides feedback systems like the dispenser, this system also has a number of different applications that could be applied to statistics and data collection around the home and office.

During the research of this project, we read a paper in which various AI sound classification techniques were used to recognise and store data about urban noise profiles. The noises from vehicles, outdoor equipment (e.g. lawnmowers), airplanes and animals were recorded, and then used to train a single-board computer, which would analyse the frequency and patterns of these events.

While it was presented more as an analysis and testing research paper for running Audio classifiers on small embedded platforms, rather than making large-scale conclusions about, for example, noise pollution in cities, it still was a super interesting read. Definitely take a peek if you’re interested in the technical side of AI. (Evaluation of Classical Machine Learning Techniques towards Urban Sound Recognition on Embedded Systems – 2019 Bruno da Silva, Axel W. Happi, An Braeken and Abdellah Touhafi).

We look forward to seeing your own AI audio creations, so be sure to tag us with @diyodemag!

Liam Davies

Liam Davies

DIYODE Staff Writer