I primarily use Perl for most of my Raspberry Pi projects, and with a growing number of quality Pi modules on CPAN and an upcoming book on Perl and Raspberry Pi programming, I expect Perl's popularity on the Pi to increase. That being said, I was bored, and wanted to use some left over components I had laying around to make my own home security system from scratch. It's in its early stages right now, and I will be adding more features later.
The IdeaThe idea is that all the sensors in the system will send their status to a centralized server via UDP messages. The server will log all the messages in text and csv format. The user interface will parse the logs using Perl regular expressions and the DBD::CSV cpan module to create graphs on the fly.
The door sensor consist of a Pi Zero W and a magnetic door contact. Whenever the door is opened, it sends a UDP message to the server with a Unix Epoch timestamp. When closed, it sends another UDP message to the server which then calculates the time it was opened.
The status report script takes a message string as an argument and sends it's status to the server using backticks like so: `perl sndstat.pl "Door is opened!"`;
#!/usr/bin/perl -w
use strict;
use IO::Socket;
my($sock, $msgserver, $msg, $port, $ipaddr, $msghost, $MAXLEN, $PORTNO, $TIMEOUT);
$MAXLEN = 1024;
$PORTNO = 8597;
$TIMEOUT = 5;
$msghost = "192.168.0.7";
$msg = $ARGV[0];
print "MSG: $msg\n";
$sock = IO::Socket::INET->new(Proto => 'udp', PeerPort => $PORTNO, PeerAddr => $msghost) or die "cannot create socket $!";
$sock->send($msg) or die "SEND: $!";
The server accepts UDP messages from all the sensors in the system using the following script:
#!/usr/bin/perl -w
use strict;
use IO::Socket;
my ($sock, $statmsg, $devaddr, $devhost, $MAXLEN, $PORTNO);
$MAXLEN = 1024;
$PORTNO = 8597;
$sock = IO::Socket::INET->new(LocalPort => $PORTNO, Proto => 'udp') or die "socket: $@";
#open log file for writing
open CQ, ">>", "security_.log" or die $!;
#autoflush file handle
select((select(CQ), $| = 1)[0]);
print "awaiting UPD messages from security devices on $PORTNO...\n";
while($sock->recv($statmsg, $MAXLEN)){
my ($port, $ipaddr) = sockaddr_in($sock->peername);
$devhost = gethostbyaddr($ipaddr, AF_INET);
print gmtime()."$devhost--> $statmsg\n";
#$sock->send("received stat ok-> $statmsg");
print CQ gmtime().",$devhost--> $statmsg\n";
}#end while
close CQ;
die "recv: $!";
The door sensor contains a TMP36 temperature sensor, and an MCP3208 12-bit ADC to read its value.
On the front porch, I used a general-purpose data logger (made with Pi Zero W) I had from another project to take temperature readings and send them to the server. It also uses the TMP36 on an external PCB and an MCP3208 ACD to read it.
I wrote my own GPIO file system interface in Perl that uses a GPIO numbering that makes sense to me. Using that script, I wrote a bit-bang driver for the MCP3208 ADC chip.
#!/usr/bin/perl -w
use strict;
=pod
gpio utilities for RPi
enable, disable, read, write
=cut
#global vars
#rpi
my @line_nums = (2,3,4,17,27,22,10,9,11,5,6,13,19,26,14,15,18,23,24,25,8,7,12,16,20,21);
my ($iox, $status);
sub gpio_enable{
my $ind = $_[0];
$iox = $line_nums[$ind];
#print "IO line: $iox\n";
my $gpiosz = @line_nums;
#print "gpiosz: $gpiosz\n";
my $direction = $_[1];
#print "direction: $direction\n";
#err chk
if($ind < 0 || $ind > $gpiosz){
print "INVALID GPIO RANGE\n";
exit;
}#end if invalid index
if($direction ne 'in' && $direction ne 'out'){
print "INVALID DIRECTION\n";
exit;
}#end if invalid direction
#write gpio pin val to export file
open(EF, ">", "/sys/class/gpio/export") or die $!;
print EF $iox;
close(EF);
#set direction of gpio pin
open(GF, ">", "/sys/class/gpio/gpio$iox/direction") or die $!;
print GF $direction;
close(GF);
}#end gpio_enable
sub gpio_disable{
}#end gpio_disable
sub gpio_read{
#gpio number as arg, returns direction in/out
my $ind = $_[0];
$iox = $line_nums[$ind];
my $value = '';
#reads state of gpio pin
open(GS, "/sys/class/gpio/gpio$iox/value") or die $!;
while(<GS>){ $value = $_; };
#print "$iox val: $value";
close(GS);
if($value == 0 || $value == 1){
return $value;
}#end unless
else{
print "ERROR: Undefined value on GPIO $iox\n";
exit;
}#end else
}#end gpio_read
sub gpio_write{
my $ind = $_[0];
$iox = $line_nums[$ind];
my $value = $_[1];
#write value to gpio pin
open(GV, ">", "/sys/class/gpio/gpio$iox/value") or die $!;
print GV $value;
close(GV);
}#end gpio_write
1;
Using the previous GPIO control script, here is the bit-bang MCP3208 driver:
#!/usr/bin/perl
use strict;
use Time::HiRes qw (usleep);
require "/home/pi/gpio/gpio_chip.pl";
=pod
routines for MCP3208 4-channel
analog to digital converter
=cut
#print "MCP3208\n-------\n";
#SPI / bitbang varables
my ($clk, $din, $dout, $chpsel);
#channels: first bit is set to single, not differential
#5 bits because the first is the start bit
my @ch0 = (1,1,0,0,0);
my @ch1 = (1,1,0,0,1);
my @ch2 = (1,1,0,1,0);
my @ch3 = (1,1,0,1,1);
my @ch4 = (1,1,1,0,0);
my @ch5 = (1,1,1,0,1);
my @ch6 = (1,1,1,1,0);
my @ch7 = (1,1,1,1,1);
#differential
my @ch01 = (1,0,0,0,0);#a0+, a1-
my @ch02 = (1,0,0,1,0);#a2+, a3-
sub read3208{;
#init empty array that will
#contain 10-bit reading from ADC
my @reading = ();
#get reading from the MCP3208 ADC
#starts comm when cs goes from high to low
#ADC channel number
#channel is an array reference
my $channel = $_[0];
#delay between clock pulses
#needs to operate at 10KHz so
#sample is accurate, so min usleep(100)
my $delay = $_[1];
#reference voltate, used only for calculations
#can be omitted
my $vref = $_[2];
#main clock variable
my $i=0;
#high flag for data gpio line
my $hf = 0;
#number of clock pulses,
my $ncp = 38; #for 20 clock pulses
#high or low state of clock pulse
my $state = 0;
my $seed = 3; #seed used to determine high or low, start 3 so first pulse is high
#toggles CS line to init communication with the adc
#start low, go high, then low. comm begins when CS
#brought from high to low
gpio_write($chpsel, 0);
usleep($delay);
#main loop
while($i<$ncp){
$state = $seed%2;#toggles between 1 and 0
$seed++;
#clock pulse high
if(($i%2) eq 0){
#print "i: $i\tcp high\tstate: $state\n";
if($channel->[$i/2] eq 1 && $i <=8){
gpio_write($din, 1);
$hf = 1;#set high flag
#print "ch[".($i/2)."]: ".$channel->[$i/2]."\n";
}#end if
if($channel->[$i/2] eq 0 && $i <=8){
gpio_write($din,0);
}#end if zero on data line
}#end if high
#if($i>8){#data line
#everything after D0 bit is don't care,
#set to state
#gpio_write($din, $state);
#}#end else
#clock pulse
gpio_write($clk, $state);
#read data out
#data clocked out on falling
#edge of clk
if(($i%2) == 1 && $i >12){
#read state of gpio pin connected
#to data out of ADC
push @reading, gpio_read($dout);
#print "i[$i]: ".$reading[($i-14)/2]."\n"
}#end read data out
#lower data if high flag set
if($hf eq 1){
#gpio_write($din,0);
$hf = 0;#reset high flag
}#end if
#pause between clk pls
usleep($delay);
#increment counter
$i++;
}#end while
#make din low
gpio_write($din, 0);
#make chip select high
gpio_write($chpsel, 1);
#perform calcuations, return a voltage
#based of 10-bit reading, and vref val
my $bindig = 2048;
my $rdgval = 0;
my $voltage = 0;
for(my $x=0; $x<12;$x++){
if(@reading[$x] == 1){
$rdgval = $rdgval + $bindig;
}#end if
$bindig = $bindig / 2;
}#end for
#voltage calculation
$voltage = ($rdgval*$vref)/4096;
return (\@reading, $rdgval, $voltage);
}#end read3208
sub init3208{
=pod
called by program before you can use the 3208
pass the gpio lines you wish to use
=cut
$clk = $_[0]; #clock
$din = $_[1]; #instructions to adc
$dout = $_[2]; #10-bit output from adc
$chpsel = $_[3]; #latch / load to init comm to adc
#init gpio lines
gpio_enable($clk, 'out');
gpio_enable($din, 'out');
gpio_enable($dout, 'in');
gpio_enable($chpsel, 'out');
#init chpsel high, comm begins when
#chipsel goes from high to low
gpio_write($chpsel, 1);
usleep(100);
}#end init3208
sub clkpls{
gpio_enable(7, 'out');
my $i=0;
while($i<1000){
gpio_write(7,1);
usleep(50000);
gpio_write(7,0);
$i++;
}#end while
gpio_write(7,0);
}#end
Here is the script on front porch temperature sensor:
#!/usr/bin/perl
use strict;
require "MCP3208.pl";
#channels: first bit is set to single, not differential
#5 bits because the first is the start bit
my @ch0 = (1,1,0,0,0);
my @ch1 = (1,1,0,0,1);
my @ch2 = (1,1,0,1,0);
my @ch3 = (1,1,0,1,1);
#init the MCP3208
init3208(10,11,12,13);
my $i=0;
open RD, ">", "temperature.log" or die $!;
my $s = 0;
for(;;){
#read ADC channel 0
my ($reading, $binval, $voltage ) = read3208(\@ch0, 50, 4.977);
#calculate temperature farenheit
my $temp = ((($voltage * 1000) - 500) / 10) * 1.8 + 32;
$temp = sprintf("%5.2f", $temp);
my $tm = gmtime();
#send status to UDP server
`perl sndstat.pl "outside temp: $temp"`;
#display reading on terminal
print "binval: $binval\tvoltage: $voltage vdc\tTemp F: $temp\n";
#write to log
print RD "$s\n$voltage\n";
#wait 5 minutes
usleep(300000000);
}
close RD;
Here are 24hrs of results from the front door (outside) temperature sensor:
Here is the live logging on the UDP server:
Next, I plan on adding cameras to the door sensors. The UDP server already has the LAMP stack up and running, so I am going to write a front end in PHP that I can view from anywhere. Boredom cured... for now.
Comments