#!/usr/bin/perl -w
#
# Send SMS using a GPRS modem (developed for Huawei E220 HSDPA USB Modem)
# 
# Note: this script currently does not work with embedded Perl - 
#    call it directly with perl.
#
# Copyright 2007, 2008 by Thomas Bleier (thomas@bleier.at)
#
# Last modified: 2008-04-21
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

package sendsms;

use strict;
use warnings;
use Getopt::Long;
use Device::SerialPort;

# globals

our $timeout = 20;
our $debug;
our $waittries = 5;
our $lastresponse = "";

# functions

sub usage {
  print "\nUsage:\n" .
    "  $0 -p /dev/ttyXX -n \"+4312345\" -m \"sms text\" [-l file] [-d] [-t x]\n" .
    "  $0 --port=/dev/ttyXX --number=\"+4312345\" --msg=\"sms text\" [--log file]\n" .
    "  $0 -p /dev/ttyXX -i [-d] [-t x]\n" .
    "  $0 -p /dev/ttyXX --pin=1234 [-d] [-t x]\n" .
    "  $0 -p /dev/ttyXX --pinquery=0/1 --pin=1234 [-d] [-t x]\n" .
    "  $0 -p /dev/ttyXX --puk=12345678 --pin=1234 [-d] [-t x]\n" .
    "  $0 -p /dev/ttyXX --nagios [-w x] [-c x] [-d] [-t x]\n" .
    "  $0 -p /dev/ttyXX --nagios [--warn=x] [--crit=x] [-d] [-t x]\n" .
    "  $0 [-h] [--help]\n" .
    "\n" .
    "  -p / --port     device of the modem to use\n" .
    "  -n / --number   telephone-number to send the SMS to\n" .
    "  -m / --msg      SMS message text to send\n" .
    "  --pin           Unlock SIM-Card using this PIN\n" .
    "  --pinquery=x    Enable (x=1) or diable (x=0) PIN query on poweron\n" .
    "  --puk           Unlock SIM-Card using this PUK (PIN also required)\n" .
    "  --nagios        Check modem connection acting like a nagios plugin\n" .
    "  -w / --warn     Signal level (%) at which nagios should report a warning (has to be > crit)\n" . 
    "  -c / --crit     Signal level (%) at which nagios should report an error (default = 0)\n" . 
    "  -l / --log      append log of sent messages to this file\n" .
    "  -d / --debug    show data sent/received from modem\n" .
    "  -t / --timeout  set timeout for modem communication (default = $timeout ms)\n" .
    "  -i / --info     show information about modem\n" .
    "  -h / --help     display this help message\n\n";
  exit 255;
}

sub getdate {
	my ($d,$m,$y,$h,$mm,$s) = (localtime)[3,4,5,2,1,0];
	return sprintf("%04d-%02d-%02d %02d:%02d:%02d",($y+1900),$m,$d,$h,$mm,$s);
}

sub serwrite {
  my ($port,$data) = @_;
  $port->write($data);
  if ($debug) {
    $data =~ s/\r/\n/;
    print "--> '$data'\n";
  }
}

sub serread {
  my ($port) = @_;
  my ($count,$str) = $port->read(32);
  my $got;
  while ($count > 0) {
    ($count,$got) = $port->read(8);
    $str .= $got;
  }
  if ($debug) {
    my $pstr = $str;
    $pstr =~ s/\r/\n/;
    print "<-- '$pstr'\n";
  }
  return $str;
}

sub waitfor {
  my ($port,$expect,$waittime) = @_;
  my ($result,$tries) = ("",$waittries);
  if (defined($waittime)) {
    $tries = $waittime / $timeout;
  }
  while (!($result =~ m/$expect/m) && ($tries > 0)) {
    $result .= serread($port);
    $tries--;
  }
  if ($result =~ m/$expect/m) {
    return 1;
  } else {
    print "Unexpected response: $result" if $debug;
    $lastresponse = $result;
    return undef;
  }
}

sub writeandwait {
  # write data in chunks of 60 bytes, reading back echo between the chunks
  # Device::SerialPort seems to work only that way
  my ($port,$data,$expect,$waittime) = @_;
  my ($rest,$result) = ($data,"");
  do {
    my $part = substr($rest,0,60);
    $rest = length($rest) > 60 ? substr($rest,60) : undef;
    serwrite($port,$part);
    $result .= serread($port);
  } while (defined($rest));
  my ($tries) = ($waittries);
  if (defined($waittime)) {
    $tries = $waittime / $timeout;
  }
  while (!($result =~ m/$expect/m) && ($tries > 0)) {
    $result .= serread($port);
    $tries--;
  }
  if ($result =~ m/$expect/m) {
    return 1;
  } else {
    print "Unexpected response: $result" if $debug;
    $lastresponse = $result;
    return undef;
  }
}

sub getresponse {
  my ($port,$tries) = @_;
  my ($result) = ("");
  $tries = $waittries unless defined($tries);
  while (!($result =~ m/^OK/m) && ($tries > 0)) {
    $result .= serread($port);
    $tries--;
  }
  if ($result =~ m/^OK/m) {
    my @lines = split(/[\r\n]+/,$result);
    return $lines[1];
  } else {
    print "Unexpected response: $result" if $debug;
    $lastresponse = $result;
    return "";
  }
}

sub open_port {
  my ($portname) = @_;
  my $serport = new Device::SerialPort($portname,1)
    || die "Can't open serial port $portname: $!\n";

  $serport->databits(8);
  $serport->baudrate(115200);
  $serport->parity("none");
  $serport->stopbits(1);
  $serport->handshake("rts");

  $serport->read_char_time(1);
  $serport->read_const_time($timeout);

  return $serport;
}

sub close_port {
  my ($serport) = @_;
  $serport->close
    || die "Error closing serial port: $!\n";
}

sub initialize_modem {
  my ($port) = @_;
  serwrite($port,"ATZ\r");
  my $initialized = waitfor($port,"^OK",500);
  if (!$initialized) {
    serwrite($port,"\cZ\n\r");
    serwrite($port,"ATZ\r");
    $initialized = waitfor($port,"^OK",500);
  }
  return $initialized;
}

sub sendsms {
  my ($portname,$smsnumber,$smstext,$logfile) = @_;
  my $result = 255;

  # cut sms message to first 160 characters
  $smstext = substr($smstext,0,160);

  if (defined($logfile)) {
    open(LOGFILE,">>$logfile")
      or die "Can't open logfile $logfile: $!\n";
    print LOGFILE getdate() . " - " . $smsnumber . " - " . $smstext;
  }

  my $port = open_port($portname);

  print "Sending SMS to $smsnumber...\n";
  if (initialize_modem($port)) {
    if (writeandwait($port,"AT+CREG?\r","^OK")) {
      if (writeandwait($port,"AT+CMGF=1\r","^OK")) {
        if (writeandwait($port,"AT+CMGS=\"$smsnumber\"\r","^>")) {
          if (writeandwait($port,"$smstext\cZ\r","^OK",5000)) {
            $result = 0;
          } else {
            $result = 5;
          }
        } else {
          $result = 4;
        }
      } else {
        $result = 3;
      }
    } else {
      $result = 2;
    }
  } else {
    $result = 1;
  }

  if (defined($logfile)) {
    print LOGFILE " - " . ($result == 0 ? "OK\n" : "ERROR ($result)\n");
  }

  close_port($port);

  if ($result == 0) {
    print "SMS Message sent successfully!\n";
  } else {
    print "Error sending SMS Message ($result)!\n";
    print "Last response from modem was: $lastresponse\n";
  }

  exit $result;
}

sub printPinStatus {
  my ($port) = @_;
  serwrite($port,"AT+CPIN?\r");
  print "PIN status: " . getresponse($port,10) . "\n";
}

sub printPinQueryStatus {
  my ($port) = @_;
  serwrite($port,"AT+CLCK=\"SC\",2\r");
  my $pinlock = getresponse($port,20);
  my $pinlocked = $pinlock =~ m/^\+CLCK\: *([01])/;
  print "PIN query on power-on: " . 
    ($pinlocked ? ($1 == 1 ? "enabled" : "disabled" ) : "unknown" ) . 
    " ($pinlock)\n";
}

sub getinfo {
  my ($portname) = @_;
  print "Getting info from modem on $portname...\n";
  my $result = 255;
  my $port = open_port($portname);
  if (initialize_modem($port)) {
    print "\n";
    serwrite($port,"AT+CGMI\r");
    print "Manufacturer: " . getresponse($port) . "\n";
    serwrite($port,"AT+CGMM\r");
    print "Model: " . getresponse($port) . "\n";
    serwrite($port,"AT+CGMR\r");
    print "Software version: " . getresponse($port) . "\n";
    printPinStatus($port);
    printPinQueryStatus($port);
    serwrite($port,"AT+CGSN\r");
    print "IMEI Number: " . getresponse($port) . "\n";
    serwrite($port,"AT+CIMI\r");
    print "IMSI Number: " . getresponse($port) . "\n";
    serwrite($port,"AT+CREG?\r");
    my $network = getresponse($port);
    my $registered = $network =~ m/^\+CREG\: *0,1/;
    print "Modem is " . ($registered ? "" : "not ") . "registered in a GSM network ($network)\n";
    serwrite($port,"AT+CSQ\r");
    my $signal = getresponse($port);
    my $sig1 = "?";
    if ($signal =~ m/^\+CSQ\: *([0-9]+)/) {
      $sig1 = $1;
    }
    print "Signal strength: $sig1 (Range: 0-31) ($signal)\n";
    serwrite($port,"AT+CSMS?\r");
    my $smsinfo = getresponse($port);
    my $support = $smsinfo =~ m/^\+CSMS\: *.,.,1,./;
    print "Sending SMS is " . ($support ? "" : "not ") . "supported ($smsinfo)\n";
    print "\n";
    $result = 0;
  } else {
    $result = 1;
    print "Error initializing modem!\n";
    print "Last response from modem was: $lastresponse\n";
  }
  close_port($port);
  exit $result;
}

sub nagiosplugin {
  my ($portname,$nagioswarn,$nagioscrit,$nagiosperf) = @_;
  my %ERRORS = ('OK'=>0,'WARNING'=>1,'CRITICAL'=>2,'UNKNOWN'=>3);
  my $result = $ERRORS{'UNKNOWN'};
  my $port = open_port($portname);
  if (initialize_modem($port)) {
    my ($manu,$model,$imei,$imsi,$signal);
    $nagioscrit = 0 unless defined($nagioscrit);
    $nagioswarn = 0 unless defined($nagioswarn);
    serwrite($port,"AT+CGMI\r");
    $manu = getresponse($port);
    serwrite($port,"AT+CGMM\r");
    $model = getresponse($port);
    serwrite($port,"AT+CGSN\r");
    $imei = getresponse($port);
    serwrite($port,"AT+CIMI\r");
    $imsi = getresponse($port);
    serwrite($port,"AT+CREG?\r");
    my $network = getresponse($port);
    my $registered = $network =~ m/^\+CREG\: *0,1/;
    if (!$registered) {
      $result = $ERRORS{'CRITICAL'};
      print "CRITICAL: modem not registered in GSM network!";
    } else {
      serwrite($port,"AT+CSMS?\r");
      my $smsinfo = getresponse($port);
      my $support = $smsinfo =~ m/^\+CSMS\: *.,.,1,./;
      if (!$support) {
        $result = $ERRORS{'CRITICAL'};
        print "CRITICAL: modem problem: SMS sending is not supported!";
      } else {
        serwrite($port,"AT+CSQ\r");
        my $signalstr = getresponse($port);
        if ($signalstr =~ m/^\+CSQ\: *([0-9]+)/) {
          $signal = int(($1 * 100) / 31);
        }
        if ($signal <= $nagioscrit) {
          $result = $ERRORS{'CRITICAL'};
          print "WARNING: modem signal ($signal%) below critical level ($nagioscrit%)!";
        } else {
          if ($signal <= $nagioswarn) {
            $result = $ERRORS{'WARNING'};
            print "WARNING: modem signal ($signal%) below warning level ($nagioswarn%)!";
          }
        }
      }
    }
    if ($result == $ERRORS{'UNKNOWN'}) {
      $result = $ERRORS{'OK'};
      print "OK: $manu $model ready to send SMS (signal: $signal%)";
    }
    if ($nagiosperf) {
      print " | signal=$signal%;$nagioswarn;$nagioscrit;0;100";
    }
    print "\n";
  } else {
    $result = $ERRORS{'CRITICAL'};
    print "CRITICAL: modem initialization failed!\n";
    print "Last response from modem was: $lastresponse\n";
  }
  close_port($port);
  exit $result;
}

sub unlocksim {
  my ($portname,$simpin,$simpuk) = @_;
  print "Unlocking SIM-Card in modem at port $portname...\n";
  my $result = 255;
  my $port = open_port($portname);
  if (initialize_modem($port)) {
    printPinStatus($port);
    print "Sending pin...\n";
    if (defined($simpuk)) {
      serwrite($port,"AT+CPIN=$simpuk,$simpin\r");
    } else {
      serwrite($port,"AT+CPIN=$simpin\r");
    }
    waitfor($port,"^OK",5000);
    printPinStatus($port);
    $result = 0;
  } else {
    $result = 1;
    print "Error initializing modem!\n";
    print "Last response from modem was: $lastresponse\n";
  }
  close_port($port);
  exit $result;
}

sub setpinquery {
  my ($portname,$simpin,$pinquery) = @_;
  print "Changing PIN query mode for modem at port $portname...\n";
  my $result = 255;
  my $port = open_port($portname);
  if (initialize_modem($port)) {
    printPinQueryStatus($port);
    print "Changing PIN query mode...\n";
    my $setflag = $pinquery eq "1" ? 1 : 0;
    serwrite($port,"AT+CLCK=\"SC\",$setflag,\"$simpin\"\r");
    waitfor($port,"^OK",5000);
    printPinQueryStatus($port);
    $result = 0;
  } else {
    $result = 1;
    print "Error initializing modem!\n";
    print "Last response from modem was: $lastresponse\n";
  }
  close_port($port);
  exit $result;
}

# configuration

my ($portname,$smsnumber,$smstext,$simpin,$simpuk,$pinquery,$nagios,$nagioswarn,$nagioscrit,$nagiosperf,$logfile,$info,$help);

Getopt::Long::Configure('bundling');
GetOptions(
           "p=s" => \$portname,      "port=s" => \$portname,
           "n=s" => \$smsnumber,     "number=s" => \$smsnumber,
           "m=s" => \$smstext,       "msg=s" => \$smstext,
                                     "pin=i" => \$simpin,
                                     "puk=i" => \$simpuk,
                                     "pinquery=i" => \$pinquery,
                                     "nagios" => \$nagios,
           "w=i" => \$nagioswarn,    "warn=i" => \$nagioswarn,
           "c=i" => \$nagioscrit,    "crit=i" => \$nagioscrit,
                                     "perf" => \$nagiosperf,
           "l=s" => \$logfile,       "log=s" => \$logfile,
           "d" => \$debug,           "debug" => \$debug,
           "i" => \$info,            "info" => \$info,
           "t=i" => \$timeout,       "timeout=i" => \$timeout,
           "h" => \$help,            "help" => \$help
          ) or usage();

if (defined($help)) {
  print "\nSend SMS using a GSM modem. Created by Thomas Bleier (thomas\@bleier.at).\n";
  usage();
}

if (defined($info)) {
  usage() unless defined($portname);
  getinfo($portname);
} elsif (defined($nagios)) {
  usage() unless defined($portname);
  if (defined($nagioswarn) && defined($nagioscrit)) {
    if ($nagioswarn <= $nagioscrit) {
      print "\nNagios warning level has to be above critical level!\n";
      usage();
    }
  }
  nagiosplugin($portname,$nagioswarn,$nagioscrit,$nagiosperf);
} elsif (defined($simpuk)) {
  usage() unless defined($portname);
  usage() unless defined($simpin);
  unlocksim($portname);
} elsif (defined($pinquery)) {
  usage() unless defined($portname);
  usage() unless defined($simpin);
  usage unless ($pinquery eq "0" || $pinquery eq "1");
  setpinquery($portname,$simpin,$pinquery);
} elsif (defined($simpin)) {
  unlocksim($portname,$simpin,$simpuk);
} else {
  usage() unless defined($portname) && defined($smsnumber) && defined($smstext);
  sendsms($portname,$smsnumber,$smstext,$logfile);
}
