First off, a correction - in my last journal entry I said the Security Labs SLW-164 was around $220. I came across the invoice and it was $160.
So let's write some code!
I'll start with the TrendNet IP501P code. It's simpler than the iGuard because it uses basic authentication instead of cookies. The code to grab the pictures should do the following:
- Since it's not infrared, it should start requesting images about 45 minutes before sunrise and finish 2 hours after sunset (I came about these 2 time frames through much experimentation, YMMV).
- Each day's pictures should be stored in its own directory.
- Test the image request result and re-request it if it's not a jpeg.
- Add a timestamp to each image.
- While testing the code for a while, make it easy to log any issues with a debug hook.
Let's take a look at each chunk of code. The full script can be found here.
#!/usr/bin/perl -w
use strict;
use LWP::UserAgent;
use HTTP::Request;
use Date::Calc qw{ Today Today_and_Now Delta_YMDHMS Add_Delta_YMDHMS System_Clock };
use Astro::Sunrise;
use Image::Magick;
use Data::Dumper;
Pretty standard start to a perl script. Date::Calc is used to figure out the time and date for directory creation and also for the timestamp that's added to each image. In the case of a non-infrared camera, it'll also be used to determine if we're even going to request an image at all. Lastly, we'll use it to figure out if it's daylight saving time or not. Astro::Sunrise is used to calculate the sunrise and sunset times for my longitude and latitude. Image::Magick will annotate the timestamp to each image.
use constant DEBUG => 1;
use constant PAUSE => 10;
use constant BASE_DIR => '/home/fliptop/tv-ip501p/';
open LOG, '>>/tmp/grab_pic_tv-ip501p.log' or die "can't open log file: $!";
print LOG "\n------------------------------------------\n" if DEBUG;
printf LOG "Today is %s/%02d/%02d\n", Today if DEBUG;
print LOG "calculating 45 minutes before sunrise and 2 hours after sunset: " if DEBUG;
Pretty self explanatory, declare a few constants like the debug hook, how often we want to request an image, and where the images will be stored. Then we open the log file and give it some initial info.
my @time = System_Clock; # dst is last value in list
my ($sunrise, $sunset) = sunrise(Today, xx.xx, yy.yy, 5, $time[8]);
my ($srh, $srm) = split /:/, $sunrise;
my ($ssh, $ssm) = split /:/, $sunset;
my @sr_adjusted = Add_Delta_YMDHMS(Today, $srh, $srm, 0, 0, 0, 0, 0, -45, 0);
my @ss_adjusted = Add_Delta_YMDHMS(Today, $ssh, $ssm, 0, 0, 0, 0, 2, 00, 0);
printf LOG "%s:%s %s:%s\n", $sr_adjusted[3], $sr_adjusted[4], $ss_adjusted[3], $ss_adjusted[4] if DEBUG;
my @start_time = ($sr_adjusted[3], $sr_adjusted[4], '00');
my @end_time = ($ss_adjusted[3], $ss_adjusted[4], '00');
Here we figure out the start/end times for requesting images. sunrise is a method in Astro::Sunrise that returns a two scalar times for sunrise/sunset when you pass in the date, your longitude, latitude, timezone, and a DST flag (which we get from the System_Clock method in Date::Calc). Make sure you substitute your own longitude/latitude values for xx.xx and yy.yy. The start/end times we'll use are stored in @start_time and @end_time.
my $uri = 'http://username:password@192.168.1.246/IMAGE.JPG';
my $ua = LWP::UserAgent->new;
$ua->agent("Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.13) Gecko/2009080317 Fedora/3.0.13-1.fc10 Firefox/3.0.13");
Here we set up the URL we'll use to request images. Note that with basic authentication the login credentials are part of the URL.
my $im = Image::Magick->new();
for (0..86400/PAUSE) {
my @Today = Today;
my @Today_and_Now = Today_and_Now;
# create dir if it doesn't exist
my $dir = sprintf "%s%s/%02d/%02d", BASE_DIR, @Today;
unless (-e $dir) {
printf LOG "creating base directory %s\n", $dir if DEBUG;
my $resp = qx{ /bin/mkdir -p $dir };
if ($resp) {
printf LOG "unable to create image dir: %s\n", $resp;
die;
}
}
After instantiating an Image::Magick object ref, we'll execute the next set of code for every image request. In this case, it'll be 86,400 seconds per day divided by the number of seconds between each request. First is to make sure the directory we'll be storing the images in exists. If it doesn't, create it. If we can't create it, something is wrong and there's no point in continuing.
if ( &_take_pic(\@Today, \@Today_and_Now) ) {
my $not_jpeg = 1;
my $res = $ua->request(HTTP::Request->new(GET => $uri));
my $status = $res->status_line;
unless ($status eq '200 OK' || $status eq '302 Found') {
printf LOG "unable to request image, status code is: %s\n", $status if DEBUG;
}
&_take_pic is a method we'll show later that figures out if the current time is between the sunrise/sunset times we declared earlier. We set a $not_jpeg flag to TRUE so we can figure out (later) if we need to request the image again (I mentioned it before, the firmware on these cheap cameras if flaky and occasionally it doesn't return what you're looking for). Then we request the image, and if it doesn't return OK or FOUND we log the status code that is returned.
while ($not_jpeg) {
my $headers = $res->{_headers};
my $ct = $headers->{'content-type'};
At first, $not_jpeg is TRUE, so we'll look at the content-type of the request result.
if ($ct eq 'text/plain') {
print LOG 'text/plain (no image) result: ', Dumper($res), "\n" if DEBUG;
sleep 2;
# request it again
$res = $ua->request(HTTP::Request->new(GET => $uri));
$status = $res->status_line;
unless ($status eq '200 OK' || $status eq '302 Found') {
printf LOG "unable to request image, status code is: %s", $status, "\n" if DEBUG;
}
}
If the content-type is plain text, we didn't get the result we were looking for, so we sleep for a couple of seconds and request it again.
elsif ($ct eq 'image/jpeg') {
print LOG 'image/jpge found: ', $headers->{'client-date'}, "\n" if DEBUG;
$not_jpeg = 0;
}
If it returns a jpeg image, all is well, and we can exit the loop.
else {
print LOG "unknown content-type: ", $ct, "\n" if DEBUG;
sleep 2;
$res = $ua->request(HTTP::Request->new(GET => $uri));
$status = $res->status_line;
unless ($status eq '200 OK' || $status eq '302 Found') {
printf LOG "unable to request image, status code is: %s", $status, "\n" if DEBUG;
}
}
}
And just in case something else is returned (besides plain text or an image), sleep a couple of seconds and try again. Now that I'm looking at all this again, it would probably be easier/simpler to do some kind of recursive call to a method for requesting the image instead of the if-then-else approach. Maybe I'll look into that later...
my $content = $res->{_content};
my $filename = sprintf "%s/%s.jpg", $dir, time;
printf LOG "writing image %s\n", $filename if DEBUG;
open BIN, ">$filename" or die "can't open binary output file: $!";
binmode BIN;
print BIN $content;
close BIN;
The content should be a binary image file, so give it a unique filename and write it to disk.
my $x = $im->Read($filename);
print LOG "$x: $filename\n" if $x && DEBUG;
$x = $im->Annotate(
text => sprintf("%s/%02d/%02d %02d:%02d:%02d", @Today_and_Now),
font => '/usr/share/fonts/dejavu/DejaVuSans-ExtraLight.ttf',
pointsize => 12,
stroke => 'white',
undercolor => 'blue',
fill => 'red',
x => 510,
y => 465
);
print LOG "$x: $filename\n" if $x && DEBUG;
$x = $im->Write($filename);
print LOG "$x: $filename\n" if $x && DEBUG;
@{$im} = (); # clear the buffer
}
sleep PAUSE;
}
Now we use Image::Magick to annotate the timestamp. I found the red/white/blue combination to be the easiest to read. I configured my camera to return images that are 640x480, so I'm placing the timestamp in the lower right-hand corner (note that when working with Image::Magick (0,0) is the upper left-hand corner!). Then we pause and do it all again.
close LOG;
exit();
sub _take_pic {
my ($todayref, $today_and_nowref) = @_;
my @Today = @{ $todayref };
my @Today_and_Now = @{ $today_and_nowref };
my @after_start = Delta_YMDHMS(@Today, @start_time, @Today_and_Now);
my @before_end = Delta_YMDHMS(@Today_and_Now, @Today, @end_time);
# time is array elements 3, 4 and 5
return 0 if $after_start[3] < 0 || $after_start[4] < 0 || $after_start[5] < 0;
return 0 if $before_end[3] < 0 || $before_end[4] < 0 || $before_end[5] < 0;
return 1;
}
The _take_pic subroutine uses the Delta_YMDHMS method of Date::Calc to see if the current time falls between what we declared earlier as the start/end time based on our sunrise/sunset calculation.
Coming up next, cookie-based authentication.