I enjoy building complex caches (and series), but I’m thinking most of my caches don’t have a broad appeal. I’m going to try and change that with a straight-forward traditional gadget cache. Spoilers follow! You may want to skip this article if you live in/near Massachusetts.

So here’s the spoiler: in order to open this cache, you must knock a certain pattern on the side of the ammo can. Nothing too complicated, but the geocacher is given no hint on the cache’s page that this is what you need to do. The title of the cache, “Shave and a Haircut“, is the only hint until you get to the cache location (GZ).

Once at the cache, there is an LCD screen which provides hints the longer you stay there. I’m going to go through each step of creating this gadget cache, including the electronics, program, and container.

Wiring the Circuit

This circuit has two components: a piezo buzzer and an LCD screen. I have never used an LCD screen before, and I have only used a piezo buzzer to create sounds, not to detect them. So both components were going to need to be experimented with.

There is sample code for an LCD screen built into the Arduino programming tool, so I gave that a shot first. I followed the wiring diagram and used the exact code. It worked great. The only thing I noticed is I had to turn the potentiometer (pot) all the way off, so I ended up getting rid of it and connected pin 3 on the LCD directly to ground.

There is also a simple “knock” tutorial from Arduino. I hooked it up as shown, and installed the code. It all worked as expected.

At this stage, I was curious how much power I was going to need to run the LCD screen and buzzer. Based on previous power supply tinkering, I figured either 1 or 2 AAs with a boost converter should do the trick, but this was a good chance to measure how many amps the components are pulling. There is no easy way to measure the amps being pulled over a USB cable connected to a computer, so I decided to cut a spare USB cable I had and hook it into a mini breadboard. There were four wires, plus bare ground. I tested with the “hot” red wire and the black ground. I didn’t use the green and white wires, which are used to transmit and receive data.

Using a multimeter, I was able to see that my computer was supplying 5.14V and 0.047A to my circuit. Multiply those numbers together, and it appears my circuit uses about 0.24 Watts of power. This means a single rechargable AA battery (1.2V) would need to supply roughly 200mA (0.24W ÷ 1.2V). Most AA batteries can supply 300mA (or even a bit more), so it looks like just one AA battery for this cache will be enough!

As you can see in the photos above, I measured the volts and amps the battery was supplying out of curiosity. It looks like my rechargable is a bit depleted because it’s only supplying 0.968V. To make up for that, the circuit is pulling 0.307A, for a total power of 0.297W supplied. Even with a depleted AA battery, this is still within acceptable range. These numbers also mean that the boost converter is about 80% efficient (0.24W ÷ 0.297W × 100%) in this case. It’s not crucial to know this, but may help with estimates in future caches.

Here is a Fritzing diagram of my completed circuit:

And here is a parts list (I have no affiliation with Adafruit or the eBay sellers):

half-size breadboard [Adafruit]

Arduino Nano 5V [eBay]

16×2 LCD Screen [Adafruit]

single aa battery holder with wires [eBay]

5v dc-dc boost converter [eBay]

large piezo buzzer [Adafruit]

resistor – 1M ohm [eBay]

resistor – 220 ohm [eBay]

Male/Male jumper wires [Adafruit]

capacitor in diagram is not required

Writing the Code

I took the basics of the code from two sources. First, and from where the idea for this cache germinated, was this article about a secret-knock gumball machine from Make magazine. This code does a lot more that I needed. I don’t have indicator LEDs or servos, and I didn’t need to be able to program a new knock into the Arduino. So, I stripped it down to just the loop which checks for a successful knock, and the success and failure methods. In particular, the method which normalizes a fast or slow knock to a standard speed for comparison was especially useful.

Next, I needed to program the LCD screen. This was my first experience with an LCD, so I stuck with the tutorial from Arduino. I then wrote a method ( LCDMessage ) which would accept a simple message and split it over two lines and center it. Click the gray bar below to see the code.

Shave and a Haircut Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 #include <LiquidCrystal.h> /* * Detects patterns of knocks and triggers responses on an LCD screen * By Eric Kristoff http://geocaching.hyliston.net * Adapted from code by Steve Hoefer http://grathio.com * Licensed under Creative Commons Attribution-Noncommercial-Share Alike 3.0 * http://creativecommons.org/licenses/by-nc-sa/3.0/us/ */ // LCD is set up on these digital pins - same as https://www.arduino.cc/en/Tutorial/LiquidCrystal LiquidCrystal lcd ( 12 , 11 , 5 , 4 , 3 , 2 ) ; // Piezo pin connection const int knockSensor = 0 ; // Piezo sensor on analog pin 0. // Constants - Could be made vars and hooked to potentiometers for soft configuration, etc. const int rejectValue = 20 ; // If an individual knock is off by this percentage of a knock we ignore. (30 is pretty lose. 10 is strict) const int averageRejectValue = 10 ; // If the average timing of the knocks is off by this percent we ignore. (20 is pretty lose, 10 is strict.) const int debounceThreshold = 80 ; // Simple debounce timer to make sure we don't register more than one knock. const int maximumKnocks = 20 ; // Maximum number of knocks to listen for. const int knockComplete = 1500 ; // Longest time to wait for a knock before we assume that it's finished. const int threshold = 40 ; // Minimum signal from the piezo to register as a knock. // Variables int knockReadings [ maximumKnocks ] ; // When someone knocks this array fills with delays between knocks. int knockSensorValue = 0 ; // Most recent reading of the knock sensor. int attemptCount = 0 ; // Count how many knock attempts have been attempted int hintCount = 0 ; // Keep track of which hints we've given // "Shave and a Hair Cut, two bits." 100=full note, 50=half note, 25=quarter note, etc. // This is the time between knocks, so there will be one less value than actual knocks. const int secretCode [ maximumKnocks ] = { 50 , 25 , 25 , 50 , 100 , 50 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 } ; void setup ( ) { lcd . begin ( 16 , 2 ) ; LCDMessage ( "Welcome to SHAVE AND A HAIRCUT!" ) ; } void loop ( ) { // Listen for any knock at all. knockSensorValue = analogRead ( knockSensor ) ; if ( knockSensorValue > threshold ) { listenToSecretKnock ( ) ; } // Offer progressive hints if ( hintCount == 3 && millis ( ) > 1200000 ) { // 20 minutes LCDMessage ( "A secret knock opens the cache!" ) ; hintCount ++ ; } else if ( hintCount == 2 && millis ( ) > 900000 ) { // 15 minutes LCDMessage ( "Notice the fist on the container" ) ; hintCount ++ ; } else if ( hintCount == 1 && millis ( ) > 600000 ) { // 10 minutes LCDMessage ( "The cache title is a hint" ) ; hintCount ++ ; } else if ( hintCount == 0 && millis ( ) > 300000 ) { // 5 minutes LCDMessage ( "Who's there? (that's a hint)" ) ; hintCount ++ ; } } // Records the timing of knocks. void listenToSecretKnock ( ) { int i = 0 ; int counter = 0 ; // First lets reset the listening array. for ( i = 0 ; i < maximumKnocks ; i ++ ) { knockReadings [ i ] = 0 ; } int currentKnockNumber = 0 ; // Increment for the array. int startTime = millis ( ) ; // Reference for when this knock started. int now = millis ( ) ; delay ( debounceThreshold ) ; do { // Listen for the next knock or wait for it to time out. knockSensorValue = analogRead ( knockSensor ) ; if ( knockSensorValue > threshold ) { // Got another knock... now = millis ( ) ; // Record the delay time. knockReadings [ currentKnockNumber ] = now - startTime ; currentKnockNumber ++ ; // Increment the counter. startTime = now ; delay ( debounceThreshold ) ; // Debounce the knock sensor. } now = millis ( ) ; // Did we timeout or have too many knocks? } while ( ( now - startTime < knockComplete ) && ( currentKnockNumber < maximumKnocks ) ) ; if ( validateKnock ( ) == true ) { triggerSuccessfulAction ( ) ; } else { triggerFailedAction ( currentKnockNumber ) ; } } // We got a good knock, so do something! void triggerSuccessfulAction ( ) { while ( true ) { // keep looping these messages LCDMessage ( "Secret knock accepted!" ) ; delay ( 4000 ) ; LCDMessage ( "Combination is: 1234" ) ; // Change this delay ( 4000 ) ; } } // We didn't like the knock. void triggerFailedAction ( int knockCount ) { knockCount ++ ; // count started at zero; let's add one if ( attemptCount < 5 || millis ( ) < 300000 ) { if ( attemptCount % 2 == 0 ) { LCDMessage ( "Who's there?" ) ; } else { LCDMessage ( "Is someone out there?" ) ; } } else if ( knockCount < 5 ) { if ( attemptCount % 2 == 1 ) { LCDMessage ( "I hear knocking. Good idea." ) ; } else { LCDMessage ( "Knocking is the right idea." ) ; } } else { if ( attemptCount % 2 == 1 ) { LCDMessage ( "The cache title is a hint." ) ; } else { LCDMessage ( "That's not the secret knock." ) ; } } } // Checks if our knock matches the secret. // Returns true if it's a good knock, false if it's not. boolean validateKnock ( ) { attemptCount ++ ; int i = 0 ; // Simplest check first: Did we get the right number of knocks? int currentKnockCount = 0 ; int secretKnockCount = 0 ; int maxKnockInterval = 0 ; // We use this later to normalize the times. for ( i = 0 ; i < maximumKnocks ; i ++ ) { if ( knockReadings [ i ] > 0 ) { currentKnockCount ++ ; } if ( secretCode [ i ] > 0 ) { secretKnockCount ++ ; } if ( knockReadings [ i ] > maxKnockInterval ) { // Collect normalization data while we're looping. maxKnockInterval = knockReadings [ i ] ; } } if ( currentKnockCount != secretKnockCount ) { return false ; // Return false if the number of knocks are wrong. } /* Now we compare the relative intervals of our knocks, not the absolute time between them. (ie: if you do the same pattern slow or fast it should still work.) This makes it less picky, which does make it less secure but also makes it less of a pain to use if you're tempo is a little slow or fast. */ boolean codeFound = true ; int totaltimeDifferences = 0 ; int timeDiff = 0 ; for ( i = 0 ; i < maximumKnocks ; i ++ ) { // Normalize the times knockReadings [ i ] = map ( knockReadings [ i ] , 0 , maxKnockInterval , 0 , 100 ) ; timeDiff = abs ( knockReadings [ i ] - secretCode [ i ] ) ; if ( timeDiff > rejectValue ) { // Individual value too far out of whack codeFound = false ; } totaltimeDifferences += timeDiff ; } // It can also fail if the whole thing is too inaccurate. if ( totaltimeDifferences / secretKnockCount > averageRejectValue ) { codeFound = false ; } if ( codeFound == false ) { return false ; } else { return true ; } } void LCDMessage ( String s ) { int pos = 0 ; int lastpos = 0 ; String line1 = "" ; String line2 = "" ; while ( pos <= 16 && pos != - 1 ) { lastpos = pos ; pos = s . indexOf ( " " , pos + 1 ) ; } if ( lastpos == - 1 ) { line1 = centerString ( s ) ; line2 = " " ; } else { line1 = centerString ( s . substring ( 0 , lastpos ) ) ; line2 = centerString ( s . substring ( lastpos + 1 , s . length ( ) + 1 ) ) ; } lcd . setCursor ( 0 , 0 ) ; lcd . print ( line1 ) ; lcd . setCursor ( 0 , 1 ) ; lcd . print ( line2 ) ; } String centerString ( String s ) { int padding = ( 16 - s . length ( ) ) / 2 ; for ( int i = 0 ; i < padding ; i ++ ) { s = " " + s ; } for ( int i = s . length ( ) ; i < 16 ; i ++ ) { s = s + " " ; } return s ; }

So, here it is: the circuit programmed with the above code. I’ve shown a few bad knocks followed by the correct knock to demonstrate how it works. The combination of the actual cache is not 1234.

Building the Container

Now that I’ve confirmed I can build the circuit and get the code to work the way I want, it’s time to build the container. My last gadget cache was a birdhouse-style container, so for this one I wanted to try something different. I had picked up an ammo can at the Army Surplus store in Kenosha, Wisconsin. It was fun flying home with it, but that’s another story. I took some measurements and decided I could put my gadget cache in there, since it will certainly be weatherproof. A small part of the ammo can will contain the chip, wires, and LCD screen, while the rest of it will be left open for the cache log and swag.

Once inside, the electronics would be very difficult to get out, but I’m not sure that’s a bad thing. I found the perfect width of plywood in my shop – it fit exactly into the ammo can. Who cares if it’s cherry veneered? I’ve had it for a few years now (from a TV console I built for my parents), and I figure it’s not going to get much other use. Once it’s spray-painted, no one will notice. Here are a few of the steps:

Putting It All Together

I had some concerns about this cache’s ability to withstand the abuse of being in an ammo can that people were going to be knocking on. In my last Arduino cache, I just left the assembled breadboard inside, and soldered the wires to all the sensors. That wouldn’t work this time. In speaking with fellow gadget geocacher th10gt, he gave me the idea to hot-glue everything in place. This has worked amazingly well.

With the circuitry all glued in place, it was time to carefully fit everything into the ammo can. I tripled checked everything because it is such a pain to remove, and this is the only way I can gain access to the electronics. I ran caulking all around the edge once it was inserted, to ensure maximum difficulty in accessing the electronics and destroying the cache.

Installing the Cache On Site

I’ve been populating a town-owned orchard with caches for several year now (with permission). I’m just about out of space, but did find an open location just about 50 feet of the trail through the woods behind some apple trees. Because this will be a non-premium traditional cache, I decided to lock it down. I installed an eye-bolt in the back of the ammo can and bought a bicycle lock. I loosely wrapped the lock’s cord around a good-sized tree, looped it through the bolt, and buried what I could. I gave it a quick test, and everything seemed good. The exposure is a bit wonky, and I covered up the combination, but here it is!