Friday, August 27, 2010

Woodpecker Puzzle Cache

I've just published my Pendeli Woodpecker puzzle cache on geocaching.com. The main idea of this cache is that you go to a specific location and at a predesignated time a device makes the sound of a woodpecker drumming on a log. The cache is located at the source of the drumming. Far enough away from the initial coordinates that simply going to the puzzle coordinates and having a manual hunt is unlikely to find the cache.
The idea for this puzzle came from the fantastic wind-up birds project by HC Gilje.

The basic components required for this project are;
  1. Atmega 328
  2. 100N capacitor
  3. 16MHz crystal
  4. Solenoid
  5. 1K resistor * 2
  6. TIP120 transistor * 2
  7. 1N4004 diode
  8. 5V Voltage Regulator (LM78S05)
  9. Real Time Clock Module
  10. Water Proof Box
The Woodpecker
The general principle is that the Atmega polls the real time clock until 10am either Saturday or Sunday. Once reached, for a 2 minute period the solenoid is powered to drum against a piece of wood.


The Woodpecker (Naked)
The solenoid is powered by 2x9V batteries and the Atmega is powered from 3 * 4.5V batteries. The RTC is powered from the same source as the solenoid but with a voltage regulator, LM78S05, bringing that down to the required 5V. This is my first project trying to use a 5V module controlled directly from an Atmega chip. Its been an interesting experience. getting everything working together and able to run uninterupted for a long time. To preserve battery power I've used full power saving features of the Atmega so everything is switched into a low power sleep state for most of the time, only checking the time once every 9 seconds.

The main circuit layout is something like (use at own risk);


I used an Arduino to program the Atmega and then extracted the chip. Not the most elegant solution but the simplest with the hardware I had to hand. Very easy to break off connections doing this though so beware. Here is the script. I'm not big on comments, sorry. The code to calculate Summer time is for Athens Greece, switch over points are the last Sundays in March and October.

 // RP  © 2010
 #include <avr/sleep.h>
 #include <avr/wdt.h>
 #include <Wire.h>
 #ifndef cbi
 #define cbi( sfr, bit ) ( _SFR_BYTE( sfr ) &= ~_BV( bit ) )
 #endif
 #ifndef sbi
 #define sbi( sfr, bit ) ( _SFR_BYTE( sfr ) |= _BV( bit ) )
 #endif
 #define DS1307_I2C_ADDRESS 0x68
 int solenoidPin = 7; // digital 7
 int SDAPin = 14 + 4; // analog 4
 int SCLPin = 14 + 5; // analog 5
 int RTCVCCPin = 14 + 3; // analog 3
 byte decToBcd( byte val )
 {
  return ( ( val / 10 * 16 ) + ( val % 10 ) );
 }
 byte bcdToDec( byte val )
 {
  return ( ( val / 16 * 10 ) + ( val % 16 ) );
 }
 volatile boolean watchdogFired = true;
 void setup()
 {
  cbi( SMCR,SE );
  cbi( SMCR,SM0 );
  sbi( SMCR,SM1 );
  cbi( SMCR,SM2 ); 
  randomSeed( analogRead( 0 ) );
  Serial.begin( 38400 );
  Wire.begin();
  beatOutHour();
 }
 boolean summerTime( byte month, byte dayOfWeek, byte dayOfMonth )
 {
  if (month < 3 || month > 10)
   return false;
  if (month == 3)
  {
   int lastSunday = (dayOfWeek == 7) ? dayOfMonth : (dayOfMonth - dayOfWeek);
   return ((31 - lastSunday) < 7);
  }
  else if (month == 10)
  {
   int lastSunday = (dayOfWeek == 7) ? dayOfMonth : (dayOfMonth - dayOfWeek);
   return ((31 - lastSunday) >= 7);
  }
  else
   return true;
 }
 void now( byte& second, byte& minute, byte& hour, byte& dayOfWeek )
 {
  pinMode( RTCVCCPin, OUTPUT );
  digitalWrite( RTCVCCPin, HIGH );
  Wire.beginTransmission( DS1307_I2C_ADDRESS );
  Wire.send( 0x00 );
  Wire.endTransmission();
  Wire.requestFrom( DS1307_I2C_ADDRESS, 7 );
  second = bcdToDec( Wire.receive() & 0x7f );
  minute = bcdToDec( Wire.receive() );
  hour = bcdToDec( Wire.receive() & 0x3f );
  dayOfWeek = bcdToDec( Wire.receive() );
  byte dayOfMonth = bcdToDec( Wire.receive() );
  byte month = bcdToDec( Wire.receive() );
  byte year = bcdToDec( Wire.receive() );
  digitalWrite( RTCVCCPin, LOW );
  pinMode( RTCVCCPin, INPUT );
  if( summerTime( month, dayOfWeek, dayOfMonth ) )
   hour++;
  Serial.print( "time: " );
  Serial.print( hour, DEC );
  Serial.print( ":" );
  Serial.print( minute, DEC );
  Serial.print( ":" );
  Serial.print( second, DEC );
  Serial.print( ", day of week:" );
  Serial.println( dayOfWeek, DEC );
  delay( 2 );
 }
 void beatOutHour()
 { 
  byte second, minute, hour, dayOfWeek;
  now( second, minute, hour, dayOfWeek );
  pinMode( solenoidPin, OUTPUT );
  for( int beat = 0; beat < hour; beat++ )
  {
   digitalWrite(solenoidPin, HIGH);
   delay( 100 );
   digitalWrite(solenoidPin, LOW);
   delay( 200 );
  }
  pinMode( solenoidPin, INPUT );
 }
 void loop()
 {
  if( watchdogFired == true )
  {
   watchdogFired = false;
   Serial.println( "loop" );
   delay( 2 );
   byte second, minute, hour, dayOfWeek;
   now( second, minute, hour, dayOfWeek );
   if( hour == 10 && minute < 2 && (dayOfWeek == 6 || dayOfWeek == 7) )
   {
    Drum();
    system_sleep( random(4) + 6 );
   }
   else
   {
    delay( 2 );
    for( int seconds = ( 60 - second ); seconds > 0; seconds -= 8 )
     system_sleep(9);
   }
  }
 }
 void Drum()
 {
  int beats = 7 + random( 9 );
  int cadence = 12 + random( 7 );
  int beatSpace = ( 1000 / cadence );
  Serial.print( "beats: " ); 
  Serial.print( beats, DEC );
  Serial.print( ", cadence: " ); 
  Serial.println( cadence, DEC );
  delay( 2 );
  pinMode( solenoidPin, OUTPUT );
  for( int beat = 0; beat < beats; beat++ )
  {
   digitalWrite(solenoidPin, HIGH);
   delay( beatSpace / 2 );
   digitalWrite(solenoidPin, LOW);
   delay( beatSpace / 2 );
  }
  pinMode( solenoidPin, INPUT );
 }
 void system_sleep(int ii)
 {
  Serial.print( "sleep: " ); 
  Serial.println( ii, DEC );
  delay( 2 );
  setup_watchdog(ii);
  cbi( ADCSRA, ADEN );
  set_sleep_mode( SLEEP_MODE_PWR_DOWN );
  sleep_enable();
  sleep_mode();
  sleep_disable();
  sbi( ADCSRA, ADEN );
 }
 // 6 = 1 sec, 7 = 2 sec, 8 = 4 sec, 9 = 8 sec
 void setup_watchdog(int ii)
 {
  byte bb;
  int ww;
  if( ii > 9 )
   ii = 9;
  bb = ii & 7;
  if( ii > 7 )
   bb |= ( 1 << 5 );
  bb |= ( 1 << WDCE );
  ww = bb;
  MCUSR &= ~( 1 << WDRF );
  WDTCSR |= ( 1 << WDCE ) | ( 1 << WDE );
  WDTCSR = bb;
  WDTCSR |= _BV( WDIE );
 }
 ISR( WDT_vect )
 {
  watchdogFired = true;
 }  



A fun and simple way to add a new dimension to your puzzle caches.

Ive two more variants on this currently in the pipeline.
1) Instead of powering a solenoid a stepper motor turns a hand cranked music box mechanism. I don't know, the idea of walking in the mountains around Athens and suddenly hearing a music box playing in the middle of nowhere seems magical to me.
2) Using a audio player to have it appear that someone is shouting "hey, its over here"

Let me know what you think. I would love to hear of anyone else taking puzzle caches to a new level.

3 comments:

  1. I've just realised that the 9V battery and regulator are going to drain far too quickly for this to live for long out in the wild. For now I've disabled the cache listing. When I have some spare time I'll try replacing the power source with 2AA batteries being boosted to 5V. Should be an improvement.

    ReplyDelete
  2. Hopefully I've fixed this now. I've updated the initial post to show the new curcuit layout. The only change is that instead of the Atmega powering to RTC from anolog port 3, analog port 3 is now used to open a transistor. The 5V regulator and RTC live behind this so the large power drain only occurs when the RTC is accessed. The sleeping Atmega using as close to nothing as possible.
    I'll check its working as expected towards the end of the week and if so I'll reenable the cache listing and go place it on the mountain again.

    ReplyDelete
  3. I'm curious. With the nature of this geocache, could you use a photovoltaic panel to charge/power this geocache or would that be cost prohibitive?

    ReplyDelete