The Birdometer v0.1a

Introduction

There’s an old Russian proverb: “Make a birdometer, it will be really fun.”

So I did.

My amazing girlfriend and I like birds. We have some hummingbird feeders. We have two Arduinos. We want to start taking data about our bird guests. So, we bought some sensors from the amazing Al Lasher’s Electronics Store in Berkeley. Then we made our “birdometer” by connecting an arduino to a passive infrared sensor, a small microphone, a Canon EOS 5D camera, and a PC. We then wrote some code to make the arduino trigger the camera shutter when the infrared sensor detects motion. We are also recording audio samples of the birds from the mic and sending them to the PC via USB.

Build

The Birdometer Breadboard

Connecting the mic is pretty easy. VCC goes to the 3.3v rail, and AUD goes to an arduino analog pin. The PIR module is also easy. VCC is also 3.3v, and OUT connects to an arduino digital pin configured in input mode. Firing the camera shutter requires a canon shutter cable, a 2.5mm jack for it to plug into, and a tiny relay which we drive with an arduino digital pin in output mode. Have a look at the breadboard drawing:

birdometer breadboard drawing

To get the right level from the mic it is good for the arduino to have an analog reference voltage. I used the 5V pin, made a voltage divider with three resistors, then fed the resulting 3.3v to the AREF pin.

The sensors are bundled together on a “probe.” The “probe” consists of an RJ45 connector, a european-style terminal block, and the two sensors. See the photo below. This way I can connect the sensors with an ethernet patch cable and move them around more easily. Here are some photos:

birdometer breadboard photo

birdometer probe photo

The Birdometer PCB

Fritzing may be the coolest thing I’ve seen all year. It is a free open source desktop app with which you lay out your project on a virtual breadboard with a virtual arduino (the drawing above comes from this). After you finish that step you route (or autoroute) your traces, then perform a design rules check. That gives you a printed circuit board layout of your project. With a few button presses you can have one made and sent to you from a factory in Germany (it costs about $34 for one arduino-shield sized board). Here’s a drawing of the Birdometer in PCB form:

birdometer pcb shield drawing

I’m going to do some more testing with the breadboard version, but then I’m totally ordering a PCB. My excitement level at this cannot be overstated.

Run

The code running on the arduino is pretty simple:

#define MIC_SIGNAL_PIN A2
#define MOTION_SENSOR_PIN 2
#define MOTION_SENSOR_METABIT 2
#define SHUTTER_OPEN_METABIT 3
#define SHUTTER_RELAY_PIN 4
#define INTERPHOTO_DELAY 2000
#define EXPOSURE_TIME 150
    
const byte delimiter[] = {254, 1, 128, 0};
int audioSample;
volatile byte meta;
volatile boolean fireShutterFlag;
unsigned long shutterTime;
unsigned long now;
    
    
void setup() {
  // Hardare setup
  analogReference(EXTERNAL); // use AREF for reference voltage
  pinMode(MOTION_SENSOR_PIN, INPUT);
  pinMode(SHUTTER_RELAY_PIN, OUTPUT);
  
  // Init interrupt handling
  meta = 0;
  fireShutterFlag = false;
  shutterTime = 0;
  attachInterrupt(0, motionSensorChange, CHANGE);
    
  // Connect to host machine
  Serial.begin(115200); 
}
    
void motionSensorChange() {
  if (digitalRead(MOTION_SENSOR_PIN)) {
    bitSet(meta, MOTION_SENSOR_METABIT);
    fireShutterFlag = true;
  } else {
    bitClear(meta, MOTION_SENSOR_METABIT);
  }
}
    
void loop() {
  //outputValue = map(sensorValue, 0, 1023, 0, 255);
  for(int x = 0; x < 1024; ++x) {
    // Handle sending the audio data sample
    audioSample = analogRead(MIC_SIGNAL_PIN);
    Serial.write((audioSample >> 8) | meta);
    Serial.write(audioSample & 255);
    
    // Handle camera shutter operation
    // Is shutter open?
    now = millis();
    if (bitRead(meta, SHUTTER_OPEN_METABIT)) {
      // Should I close it?
      
      if (now - shutterTime >= EXPOSURE_TIME) {
        digitalWrite(SHUTTER_RELAY_PIN, LOW);
        bitClear(meta, SHUTTER_OPEN_METABIT);
        shutterTime = now;
      }
    } else {
      // Should I open it?
      
      if (fireShutterFlag && now - shutterTime >= INTERPHOTO_DELAY) {
        fireShutterFlag = false;
        bitSet(meta, SHUTTER_OPEN_METABIT);
        shutterTime = now;
        digitalWrite(SHUTTER_RELAY_PIN, HIGH);
      }
    }
    
  }
    
  Serial.write(delimiter, 3);
}

The code running on the PC is also pretty simple:

#!/usr/bin/python
""" Read some delimited values from the serial port """
import serial
import wave
import struct
import time
import math
import itertools
import sys
    
    
def write_wave_data(samples, wave_fh):
    """
    Write wave frames to the given wave module filehandle, return byte count
    """
    global SAMPLES_WRITTEN
    
    try:
    
        for left, right in samples:
            wave_fh.writeframesraw(struct.pack('hh', left, right))
                                   # *[channel for channel_tuple in samples
                                   #   for channel in channel_tuple]))
            SAMPLES_WRITTEN += 1
    except:
        #print samples
        raise
    
    return
    
    
def u10_to_s16(val):
    """ Return input scaled from range [0,1023] to [-32767,32767] """
    return int(((float(val) / 1023.0) - 0.5) * 65535.0)
    
    
def float_to_s16(val):
    """ Return input scaled from range [-1.0,1.0] to [-32767,32767] """
    return int(val * 32767.0)
    
    
def serial_to_wave(serial_fh, wave_fh, byte_read_size):
    """
    Read from given pyserial filehandle, pass data to write_wave_data
    
    The source sends a stream of bytes. The samples are 10 bits long and are
    sent in byte pairs (total 16 bits) so that no sample data is lost. The
    sample data is send big-endian. The first byte contains the
    most-significant bits and the second byte contains the rest. The first
    6 bits of the first byte are either unsed or are used to pass non-audio
    data.
    
    This is an example of the 16 bits we receive. Positions with an x are
    unused or metadata, the numeric positions are audio sample data.
    
    byte 1    byte 2
    --------  --------
    xxxxxx12  34567890
    """
    delimiter = bytearray([254, 1, 128])
    sine = sine_gen(amplitude=0.05, shift=0.11, frequency=5000)
    
    sine_next = sine.next
    # def sine_next():
    #     val = sine.next()
    #     print "sine next, {}".format(val)
    #     return val
    
    meta0_tone = lambda meta: float_to_s16(sine_next()) if (meta & 1) else 0
    
    while True:
        buf = serial_fh.read(3)
        if buf == delimiter:
            samples = bytearray(serial_fh.read(byte_read_size))
            if len(samples) != byte_read_size:
                print "Drop {} bytes:Bad sample count".format(len(samples))
                continue
    
            # Turns a list of byte pairs into a list of tuples in the form:
            # [xxxxxxyy, yyyyyyyy, ...] into
            # [(int from xxxxxx, int from yy yyyyyyyy), ...]
            # a.k.a. [(metadata, audiosample), ...]
    
            # for high, low in zip(*(iter(samples),) * 2):
            #     print high, low
            #     print "\t", high >> 2, ((high & 3) << 8) | low
    
            sample_data = [(high >> 2, ((high & 3) << 8) | low) for high, low
                           in zip(*(iter(samples),) * 2)]
    
            write_wave_data([(meta0_tone(meta), u10_to_s16(audio))
                             for meta, audio in sample_data],
                            wave_fh)
    
            sys.stderr.write("+")
        else:
            sys.stderr.write("-")
    
    
def sine_gen(frequency=440.0, framerate=44100.0, amplitude=0.5, shift=0.0):
    """
    Returns a sine_wave generator. Adapted from sine_wave function here:
    http://zacharydenton.com/generate-audio-with-python/
    requires math and itertools
    """
    sine = math.sin
    pi = math.pi
    count = itertools.count
    
    if 1 > amplitude < 0:
        raise OverflowError("amplitude must be 0-1: given".format(amplitude))
    frequency = float(frequency)
    framerate = float(framerate)
    amplitude = float(amplitude)
    shift = float(shift)
    
    for i in count(0):
        yield sine(2 * pi * (frequency * (i / framerate))) * amplitude + shift
    
    
def main():
    """
    Indefinitely read from the serial port, convert the data, write it to an
    output wave file
    """
    
    ser = serial.Serial('/dev/tty.usbmodem621', 115200, timeout=1)
    wav = wave.open("out-{}.wav".format(int(time.time())), 'w')
    wav.setparams((2, 2, 5900, -1, 'NONE', 'not compressed'))
    read_size = 2048  # THIS SHOULD BE 2X THE ARDUINO WRITE SIZE
    
    try:
        start_time = time.time()
        serial_to_wave(ser, wav, read_size)
    finally:
        print "\n\n{} Hz".format(SAMPLES_WRITTEN / (time.time() - start_time))
        print "Closing serial connection and wav file.\n"
        ser.close()
        wav.close()
    
    
if __name__ == '__main__':
    SAMPLES_WRITTEN = 0  # a gross but easy solution
    main()

Next

In my next post I’ll show what it looks like and post some more example shots and sounds. Here are some resources I used:

General Arduino Docs

Analog Arduino Reads

Info About Resistor-based Voltage-dividers

Using Python to Make Wave Files