The Birdometer v0.1a
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:
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:
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:
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: