Three puzzle levels to challenge your dog. Dog barking, doorbells, bicycle bells or cat meows are played to break their concentration.
In addition to three classic dog puzzles, CanineQuest offers the ability to play audio files in a continuous loop – either immediately or with a delay. This is designed to challenge your dog and help desensitize them to triggering sounds. The delayed reward should capture their attention – not the distracting noise.
CanineQuest is a dog toy that challenges a dog's intelligence. Three different puzzle levels offer something for every dog. A sliding puzzle, a bayonet lock puzzle and a secured drawer puzzle.
A sound board is built in to break the dog's concentration and challenge it further. You can play your own sounds on it. The sounds can be delayed by up to one minute using the delay function.
The volume is set using a rotary switch. The standard audio control buttons are installed.
The charge level and charge control of the built-in battery can be checked on the underside. To prevent the animal from reaching the battery, the charging socket is secured with a sliding plate.
To prevent damage to the surface on which it is played, the feet are fitted with felts.
The device is NOT intended for unsupervised play. Care has been taken to ensure that most of the parts are too large to swallow. However, rechargeable batteries are installed and these can harm animals, people and the environment if damaged. For this reason, CanineQuest should only be used when accompanied by a human.
Mechanical | |
Name | Quantity |
M3 threaded insert | 34 |
M3 x 12mm pan-head screw | 8 |
M3 x 6mm pan-head screw | 20 |
M3 x 25mm cylinder head screws | 4 |
M3 x 35mm cylinder head screws | 4 |
20mm felt glides | 8 optional |
cotton cord 5mm | >6m |
Electrical | |
Arduino Nano | 1 |
DFPlayer | 1 |
1kOhm resistor | 1 |
Speaker - 3W 8Ω | 1 |
Lithium battery shield for 2x 18650 | 1 |
18650 lithium-ion battery 2900mAh | 2 |
10k Ohm rotary potentiometer | 2 |
Pushbutton IP67 stainless steel 16mm 2P | 2 |
Pushbutton with RGB illumination 5V - stainless steel | 4 |
KW1-103-7 Micro roller switch | 1 optional |
WAGO 221-413 | 1 |
WAGO 221-415 | 1 |
Various strands of wire | |
Solder | |
Cable ties |
Tools |
Name |
3D printer with minimum 200mmx200mm buildplate |
Soldering iron |
Knife for cleaning edges |
Allen key set |
Side cutter |
Wire stripper |
Software | |
Name | Version |
Arduino IDE | 1.8.19 was used for the development |
PrusaSlicer | 2.8.1 was used for the development |
KiCad | Not relevant for recreating, but used in prototyping |
Onshape | Not relevant for recreating, but used in prototyping |
//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%CanineQuest%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%//
//Spring 2025
//https://www.printables.com/@BGDglider_2502284
//######################################################################//
//######################################################################//
///////////////////////////////////INIT///////////////////////////////////
//######################################################################//
//######################################################################//
/****************************** Libraries *******************************/
#include "SoftwareSerial.h"
#include "DFRobotDFPlayerMini.h"
/*************************** Communication ******************************/
SoftwareSerial mySoftwareSerial(10, 11); // RX, TX
DFRobotDFPlayerMini myDFPlayer;
int pause = 0;
/************************** Declare button ******************************/
const int playButton = 7; // Play Button
const int stopButton = 2; // Stop Button
const int nextButton = 4; // Next Button
const int prevButton = 5; // Previous Button
/****************************** RGB LED *********************************/
const int greenPin = 3;
const int redPin = 9;
const int bluePin = 6;
unsigned long ledTimer = 0; // Timer for LED flashing intervals
const int ledStopInterval = 800; // Interval in milliseconds for stop flashing
const int ledWaitInterval = 200; // Interval in milliseconds for wait flashing
const int ledFaultInterval = 500; // Interval in milliseconds for fault flashing
int ledState = 0; // Status LED: 0 = stopped, 1 = playing song, 2 = error
/*********************** Declare potentiometer **************************/
const int volumePin = A3; // Potentiometer 1 for volume
const int timingPin = A1; // Potentiometer 2 for song delay
int volumeValue = 0;
int timingValue = 0;
unsigned long previousMillis = 0; // Start time of the timer
unsigned long delayTime; // Variable for the delay (dynamically controlled)
/*************************** Declare debounce ***************************/
bool debounceButton(int buttonPin) {
static unsigned long lastPressTimes[6] = {0}; // Array for several buttons
const int debounceDelay = 200; // Waiting time in milliseconds
int buttonIndex = buttonPin - 2; // If push-button pins start at 2 (2 = index 0, 3 = index 1, etc.)
if (digitalRead(buttonPin) == LOW) { // Button pressed
if (millis() - lastPressTimes[buttonIndex] > debounceDelay) {
lastPressTimes[buttonIndex] = millis();
return true; // Valid actuation
}
}
return false; // No valid button press
}
//######################################################################//
//######################################################################//
///////////////////////////////////SETUP//////////////////////////////////
//######################################################################//
//######################################################################//
void setup() {
// Serial communication with the module
mySoftwareSerial.begin(9600);
// Initialize Arduino serial
Serial.begin(115200);
/************************* Initialize button **************************/
pinMode(playButton, INPUT_PULLUP);
pinMode(stopButton, INPUT_PULLUP);
pinMode(nextButton, INPUT_PULLUP);
pinMode(prevButton, INPUT_PULLUP);
/*************************** Initialize LED *****************************/
pinMode(redPin, OUTPUT);
pinMode(greenPin, OUTPUT);
pinMode(bluePin, OUTPUT);
/********************** Initialize potentiometer ************************/
pinMode(volumePin, INPUT); // Pin for the volume potentiometer
pinMode(timingPin, INPUT); // Pin for the delay potentiometer
/************************** Audio Management *****************************/
if (!myDFPlayer.begin(mySoftwareSerial)) {
while (true)
;
}
myDFPlayer.setTimeOut(500); // Serial timeout 500ms
myDFPlayer.volume(10); // Volume 10
myDFPlayer.EQ(0); // Normal equalization
myDFPlayer.play(1); // Spielt Song 1 beim Starten
myDFPlayer.enableLoop(); // Aktiviert das Looping für den Song
/************************ LED Startup Pulse *****************************/
for (int brightness = 0; brightness <= 255; brightness++) { // Increase brightness from 0 to 255
analogWrite(greenPin, brightness);
delay(5); // Smooth transition
}
for (int brightness = 255; brightness >= 0; brightness--) { // Increase brightness from 255 to 0
analogWrite(greenPin, brightness);
delay(5); // Smooth transition
}
}
//######################################################################//
//######################################################################//
///////////////////////////////////LOOP///////////////////////////////////
//######################################################################//
//######################################################################//
void loop() {
/**************************** Potentiometer *****************************/
// Adjust volume via potentiometer
volumeValue = analogRead(volumePin); // Read the value of the volume potentiometer
int vol = map(volumeValue, 0, 1023, 0, 30); // Mapping to the 0-30 range (volume control)
myDFPlayer.volume(vol);
/************************ Potentiometer und Timer ***********************/
timingValue = analogRead(timingPin); // Read the value of the timing potentiometer
delayTime = map(timingValue, 0, 1023, 1000, 60000); //Mapping: 1 second to 60 seconds
if (millis() - previousMillis >= delayTime) {
previousMillis = millis(); // Reset timer
if (myDFPlayer.readState() == 512) { // When song finished
myDFPlayer.start(); // Repeat song
}
}
/********************************* LED **********************************/
int16_t playerState = myDFPlayer.readState(); // Lesen des aktuellen Player-Status
switch (playerState) {
case 513: // Song spielt
digitalWrite(redPin, LOW);
digitalWrite(greenPin, LOW);
digitalWrite(bluePin, HIGH);
break;
case 512: // Delay active
if (millis() - ledTimer > ledWaitInterval) {
ledTimer = millis();
digitalWrite(bluePin, !digitalRead(bluePin));
}
break;
case 514: // Song stopped
if (millis() - ledTimer > ledStopInterval) {
ledTimer = millis();
digitalWrite(bluePin, !digitalRead(bluePin));
}
break;
case 0: // Error state
if (millis() - ledTimer > ledFaultInterval) {
ledTimer = millis();
digitalWrite(redPin, !digitalRead(redPin));
}
break;
default:
digitalWrite(redPin, LOW);
digitalWrite(greenPin, LOW);
digitalWrite(bluePin, LOW);
break;
}
/***************************** Status button ****************************/
bool playPressed = debounceButton(playButton);
bool stopPressed = debounceButton(stopButton);
bool nextPressed = debounceButton(nextButton);
bool prevPressed = debounceButton(prevButton);
/******************************* STOP ***********************************/
if (stopPressed) {
myDFPlayer.stop();
}
/*************************** PAUSE-CONTINUE *****************************/
if (playPressed) {
pause = !pause;
if (pause == 0) {
myDFPlayer.start();
}
if (pause == 1) {
myDFPlayer.pause();
}
}
/*************************** Previous ***********************************/
if (prevPressed) {
myDFPlayer.previous();
}
/********************************* Next *********************************/
if (nextPressed) {
myDFPlayer.next();
}
}
Sliding Pivot | |
![]() | |
Material | PLA |
Nozzle | 0.40mm |
Layer Height | 0.10mm |
Infill | 15% |
Supports | No |
Brim | No |
Quantity | 1 |
MMU | There are two versions. One without logo and one with logo, the logo can be colored. |
Print Orientation | Standing Normal |
Bayonet lever | |
![]() | |
Material | PLA |
Nozzle | 0.40mm |
Layer Height | 0.10mm |
Infill | 15% |
Supports | Yes |
Brim | No |
Quantity | 2 |
MMU | No |
Print Orientation | Upside down |
Base | |
![]() | |
Material | PLA |
Nozzle | 0.40mm |
Layer Height | 0.10mm |
Infill | 15% |
Supports | Yes (Everywhere) |
Brim | Yes |
Quantity | 1 |
MMU | No |
Print Orientation | Standing Normal |
Middle | |
![]() | |
Material | PLA |
Nozzle | 0.40mm |
Layer Height | 0.10mm |
Infill | 15% |
Supports | Yes (Everywhere) |
Brim | Yes |
Quantity | 1 |
MMU | No |
Print Orientation | Standing Normal |
Top | |
![]() | |
Material | PLA |
Nozzle | 0.40mm |
Layer Height | 0.10mm |
Infill | 15% |
Supports | Yes (Everywhere) |
Brim | Yes |
Quantity | 1 |
MMU | Operating aids and the logo are embedded in the top and can be colored as desired. |
Print Orientation | Upside down |
Speaker cover | |
![]() | |
Material | PLA |
Nozzle | 0.40mm |
Layer Height | 0.10mm |
Infill | 15% |
Supports | No |
Brim | No |
Quantity | 1 |
MMU | The logo is embedded in the speaker cover and can be colored as desired. |
Print Orientation | Upside down |
Push-button panel | |
![]() | |
Material | PLA |
Nozzle | 0.40mm |
Layer Height | 0.10mm |
Infill | 15% |
Supports | No |
Brim | Yes |
Quantity | 1 |
MMU | No |
Print Orientation | If textured sheet metal, on visible side, otherwise inside |
Potentiometer panel | |
![]() | |
Material | PLA |
Nozzle | 0.40mm |
Layer Height | 0.10mm |
Infill | 15% |
Supports | No |
Brim | Yes |
Quantity | 1 |
MMU | No |
Print Orientation | If textured sheet metal, on visible side, otherwise inside |
Potentiometer knob | |
![]() | |
Material | PLA |
Nozzle | 0.40mm |
Layer Height | 0.10mm |
Infill | 15% |
Supports | No |
Brim | No |
Quantity | 2 |
MMU | A line is embedded in the potentiometer knob. This can be colored. |
Print Orientation | Standing |
Charging socket Cover | |
![]() | |
Material | PLA |
Nozzle | 0.40mm |
Layer Height | 0.10mm |
Infill | 15% |
Supports | No |
Brim | No |
Quantity | 1 |
MMU | A battery drawing is embedded. This can be colored. |
Print Orientation | Outside top |
Drawer | |
![]() | |
Material | PLA |
Nozzle | 0.40mm |
Layer Height | 0.10mm |
Infill | 15% |
Supports | Yes (Everywhere) |
Brim | No |
Quantity | 1 |
MMU | No |
Print Orientation | Standing Normal |
Drawer Front | |
![]() | |
Material | PLA |
Nozzle | 0.40mm |
Layer Height | 0.10mm |
Infill | 15% |
Supports | No |
Brim | Yes |
Quantity | 1 |
MMU | No |
Print Orientation | On visible side |
Drawer Pin | |
![]() | |
Material | PLA |
Nozzle | 0.40mm |
Layer Height | 0.10mm |
Infill | 15% |
Supports | No |
Brim | No |
Quantity | 1 |
MMU | No |
Print Orientation | Standing Normal |
PCB-Holder Nano | |
![]() | |
Material | PLA |
Nozzle | 0.40mm |
Layer Height | 0.10mm |
Infill | 15% |
Supports | No |
Brim | No |
Quantity | 1 |
MMU | No |
Print Orientation | Prone Normal |
PCB-Holder DFPlayer | |
![]() | |
Material | PLA |
Nozzle | 0.40mm |
Layer Height | 0.10mm |
Infill | 15% |
Supports | No |
Brim | No |
Quantity | 1 |
MMU | No |
Print Orientation | Prone Normal |
Feet | |
![]() | |
Material | PLA |
Nozzle | 0.40mm |
Layer Height | 0.10mm |
Infill | 15% |
Supports | No |
Brim | No |
Quantity | 8 |
MMU | No |
Print Orientation | Standing Normal |
Start with the base, it needs 14 threaded inserts. 4 for the battery mounting, 4 for the speaker mounting, 2 for the circuit board aids (optional) and 2 each for the front mounting of the switch plate and potentiometer plate.
It requires 8 inserts on the underside to attach the felt feet.
The two WAGOs are glued in the middle. The 5 will be the - terminal block and the 3 will be the + terminal block.
I recommend testing the electrical setup on a breadboard beforehand. The buttons and potentiometers are inserted into the planned potentiometer panel and push-button panel. The PCB-holder aids can be used to hold the PCB in place if necessary. Fix the PCB with hot glue if necessary. Here it pays to use wires that are not too thick (0.75 mm2 is the maximum ;-) ) and to plan some reserve. In this configuration, the electrics can be tested in the devinitive position before the device is closed.
Two potentiometer knobs can be plugged onto the potentiometers. The potentiometer knobs can be colored with MMU so that a line indicates the position.
4 threaded inserts are used here.
The drawer and drawer front are glued here.
A cord is threaded through the drawer holes and tied at the front. A plait is then knotted according to taste. A diamond knot with 2 cords was knotted here.
It is important to install the following parts before connecting the base and the middle:
Install the whole electrical setup minus potentiometer panel and push-button panel in the base
Insert the charging socket cover into the guide in the base
Insert the drawer into the drawer recess in the middle
Leave the wires labeled looking out of the corresponding openings so that the button plate and the potentiometer plate can be easily connected after the middle and base have been mounted.
Up to this point, a USB cable can also be connected to the nano so that code adjustments can still be made.
8 feet are attached to the base with M3 x 12mm pan-head screws.
Then glue on the felts.
A cord was also passed through the drawer pin and tied to the front. A diamond knot pattern was also knotted here with a second cord.
Cotton cord is also glued onto the bayonet lever. This means that any bites are not made directly into the PLA, but are cushioned in the cotton. The sliding pivot should now also be ready to be installed straight away.
The speaker cover is installed in the next step. If an MMU is available, the part can be colored accordingly.
With MMU function the CanineQuest logo and the operating pictograms can be colored.
4 threaded inserts are used here. Further 2 each for the front mounting of the switch plate and potentiometer plate.
Now the speaker cover can be glued to the top.
Sliding pivot must be placed on the middle here, after which it can no longer be inserted.
The top can now be placed on the middle and screwed down from below via the last holes using M3 x 35mm cylinder head screws.
Before the dog can solve the puzzles, the trainer must use the Play, Stop, Next, Previous buttons to select the sound that is to distract the dog. In addition, an appropriate volume should be set using the volume control. Then set the delay between 0-60 seconds with the Delay control. If the puzzle has been prepared with food, it is now ready to be presented to the dog.
While the dog is trying to solve the three puzzle levels, the sound can be changed and the delay and volume can be adjusted.
Repeat until the desired training result has been achieved :).
The built-in LED indicates the status of the CanineQuest.
Green | Startup |
Blue | Plays audio |
Flashing blue 800ms | Audio stopped |
Flashing blue 200ms | In the delay before new audio is played |
Flashing Red 500ms | Malfunction |
Open the charging cover
Either charging via micro usb or usb c
The charge status of the CanineQuest can be read on the underside. The 5 charge status LEDs can be seen through the clear PLA.
The SD card must be empty. The desired audio files are then saved on it. The files must be labeled as follows: 001, 002, 003, 004, 005, 006 and so on.
Further information can be found in the DFPlayer manual.
If the LED flashes red, an error has been detected.
-Is the SD card inserted and formatted correctly?
-Is the DFPlayer correctly connected to the Nano?
Our dog was disturbed by a very specific bird call. The goal of this project was to desensitize him to that sound in a naturally playful way.
I used the following PLA in my prototype:
The author marked this model as their own original creation.