Arief Yudhawarman

Masih belajar buat blog

SMS Gateway dengan Perl

leave a comment »


SMS Gateway

Pada pertengahan tahun 2005 penulis mengerjakan proyek SMS Gateway untuk proses perhitungan suara pada Pilkada di Jember. Pada saat itu penulis menggunakan aplikasi SMS Server Tool (smstools) dan modem GSM Siemens MC35i dan TC35i. Harganya per biji waktu itu 2,1 juta.

Modem GSM Siemens

Modem GSM Siemens

Dibandingkan dengan aplikasi sms server saat ini seperti kannel atau gammu, smstools sangatlah sederhana. Aplikasi smstools-1.14.3.tar.gz yang saat itu penulis pakai hanya mempunyai fitur konfigurasi sms. Untuk pengolahan sms yang masuk dilakukan melalui script programming seperti bash atau perl begitu pula untuk menyimpan sms ke dalam database seperti MySQL.


Modem GSM

Beberapa tahun kemudian penulis mendapatkan tugas di kantor membuat SMS Gateway untuk broadcast sms internal ke staff kantor baik pusat maupun cabang. Dengan memperhatikan kepraktisan dan kemudahan penulis menggunakan kannel dan playSMS sedangkan modem gsm menggunakan “Wavecom Fast Track” dengan koneksi serial.

Wavecom Fast Track

Wavecom Fast Track


Penulis juga telah mencoba modem gsm itegno dengan koneksi USB meski menggunakan perangkat yang murah ini aplikasi sms gateway dapat berjalan dengan lancar.

modem gsm itegno

modem gsm itegno


SMS Error

Setelah beberapa kali menggunakan playSMS untuk mengirimkan sms broadcast terlihat di menu All outgoing SMS banyak sms yang tidak terkirim dengan status pending atau failed. Ini membuat penulis penasaran dengan langsung melihat isi file /var/log/kannel/kannel.log.

Ada tiga macam error yang terdeteksi di sini:

  1. CMS ERROR

    2015-06-05 14:39:22 [3005] [7] ERROR: AT2[smsgw]: CMS ERROR: +CMS ERROR: 512
    2015-06-05 14:39:22 [3005] [7] ERROR: AT2[smsgw]: CMS ERROR: User abort (512)
    

  2. Generic error

    2015-06-05 15:00:41 [3005] [7] ERROR: AT2[smsgw]: Generic error: ERROR
    

  3. CME ERROR

    2015-08-21 16:27:47 [17816] [7] ERROR: AT2[smsgw]: Generic error: +CME ERROR: 515
    

Mengenai CME ERROR dijelaskan oleh Mas Asfihani di artikelnya Kannel +CME ERROR: 515 Issue.

This is only occurs when I have submitted several (three or more) messages through playsms interface on Itegno 3000 GSM modem (this is Wavecom chipset modem actually). This problem doesn’t exist if I send one or two messages only. Or, if I send broadcast messages, the kannel only sent two first seen messages and forgot another🙂. Maybe the modem is slow enough to receive several messages in a time.

Untuk mengatasi masalah di atas dia menyarankan untuk melakukan patch terhadap source kannel.

Namun bagaimana mengatasi error yang timbul seperti nomor 1 dan 2 di atas? Dan apakah aplikasi sms menyimpan status error seperti “CMS ERROR” sehingga kita bisa mengetahui sms yang ditujukan ke nomor berapa saat error tersebut muncul? Kemudian juga apakah maksud status pending atau failed? Apakah itu berarti nomor yang dituju sudah expired atau tidak aktif (off)? Jika kita mengetahui status sebenarnya (lihat tabel SMS Delivery Status di bawah) maka kita bisa mengeluarkan nomor kontak tersebut dari daftar penerima sms.

Tambahan lagi ada operator selular yang membatasi pengiriman sms dalam waktu tertentu:


Instalasi SMS Gateway

Berdasarkan beberapa masalah yang kerap ditemui saat menggunakan kannel dan playSMS sedangkan broadcast sms untuk staf kantor sangat diinginkan terkirim dengan benar ke nomor tujuan maka penulis berinisiatif membuat sendiri aplikasi SMS Gateway dengan kriteria sbb:

  • Ada jeda waktu beberapa detik antara pengiriman sms. Ini untuk menghindari modem memberi respon CME ERROR 515 dan juga untuk mencegah agar nomor tidak diblokir oleh operator selular karena keseringan kirim “spam”.
  • Setelah pengiriman beberapa sms modem akan direstart untuk menghindari modem hang.
  • Setelah uptime beberapa jam modem akan direstart.

Ada keterbatasan dalam pembuatan aplikasi ini:

  • Jumlah karakter yang dikirim paling banyak 160 karakter sehingga jika lebih dari itu pesan akan dipotong.
  • Default alphabet 7 bit (data coding).

Berikut tahap instalasi SMS Gateway jika menggunakan distro Debian:

  1. Instalasi package debian dengan apt-get

    apt-get install libdbi-perl libdbd-sqlite3-perl sqlite3 build-essential

  2. Instalasi module CPAN
  3. Setup file database /var/log/sms.db dengan sqlite3.

    sqlite3 /var/log/sms.db


    Setelah masuk console sqlite masukkan perintah sql di bawah ini:

    CREATE TABLE log (
      no INTEGER PRIMARY KEY,
      text VARCHAR(640),
      datetime DATETIME not null);
    CREATE TABLE smsin (
       no INTEGER PRIMARY KEY,
       sender CHAR(14),
       text VARCHAR(160),
       tpscts DATETIME);
    CREATE TABLE smsout ( 
      no INTEGER PRIMARY KEY,
      reference INTEGER,
      destination CHAR(14),
      text VARCHAR(160),
      status CHAR(2),
      tpscts DATETIME,
      tpdt DATETIME);
    

    Untuk keluar dari console sqlite3 masukkan perintah .quit.

  4. Hubungkan modem GSM ke komputer. Jika modem GSM menggunakan koneksi usb cek isi perintah dmesg untuk mencari port usb yang digunakan oleh modem tersebut:
    [10806337.666912] usb 1-3: USB disconnect, device number 14
    [11644270.884077] usb 6-1: new full-speed USB device number 2 using uhci_hcd
    [11644271.046096] usb 6-1: New USB device found, idVendor=0eba, idProduct=1080
    [11644271.046100] usb 6-1: New USB device strings: Mfr=1, Product=2, SerialNumber=0
    [11644271.046103] usb 6-1: Product: USB-Serial Controller
    [11644271.046105] usb 6-1: Manufacturer: Prolific Technology Inc.
    [11644271.100537] usbcore: registered new interface driver usbserial
    [11644271.100550] USB Serial support registered for generic
    [11644271.100811] usbcore: registered new interface driver usbserial_generic
    [11644271.100813] usbserial: USB Serial Driver core
    [11644271.112478] USB Serial support registered for pl2303
    [11644271.112613] pl2303 6-1:1.0: pl2303 converter detected
    [11644271.124253] usb 6-1: pl2303 converter now attached to ttyUSB0
    [11644271.124400] usbcore: registered new interface driver pl2303
    [11644271.124402] pl2303: Prolific PL2303 USB to serial adaptor driver
    


    Berdasarkan keluaran dmesg modem GSM tersebut menggunakan device /dev/ttyUSB0 untuk komunikasi serial.

  5. Kita bisa mencek spesifikasi modem GSM dengan minicom. Aplikasi ini belum tentu terinstal jadi instal terlebih dahulu:

    apt-get install minicom


    Buat file /etc/minirc.dfl dengan nano atau vi. Sesuaikan nama port, jika menggunakan koneksi serial maka umumnya adalah /dev/ttyS0:

    # Machine-generated file - use "minicom -s" to change parameters.
    pr port             /dev/ttyUSB0
    pu baudrate         115200
    pu bits             8
    pu parity           N
    pu stopbits         1
    
  6. Jalankan minicom dan masukkan perintah yang berhuruf tebal di bawah ini:
    ati0                                                      
     WAVECOM MODEM                                            
    
     MULTIBAND  900E  1800 
    
    OK
    
    ati1
    OK
    
    ati2
    OK
    
    ati3
    651b09gg.Q2403B 244 062910 13:34
    
    OK
    
    ati4
    Q:0 V:1 S0:000 S2:043 S3:013 S4:010 S5:008
    +CR:0 +CRC:0 +CMEE:0 +CBST:0,0,1
    +SPEAKER:0 +ECHO:0,1 &C:1 &D:2 %C:0
    +IPR:115200 +ICF:3,4 +IFC:2,2
    
    OK
    
    ati5
    Q:0 V:1 S0:000 S2:043 S3:013 S4:010 S5:008
    +CR:0 +CRC:0 +CMEE:0 +CBST:0,0,1
    +SPEAKER:0 +ECHO:0,1 &C:1 &D:2 %C:0
    +IPR:115200 +ICF:3,4 +IFC:2,2
    
    OK
    
    ati6
    DATA RATES: AUTOBAUD,300,1200,1200/75,2400,4800,9600,14400
    DATA MODES: T/NT,ASYNCHRONOUS
    FAX CLASS: 1,2
    
    OK
    
    ati7
    SPEECH CODINGS: FR,EFR,HR
    
    OK
    

  7. Edit file script smsgateway.pl. Simpan di direktori /usr/local/sbin dan set file permission ke 755.
    #!/usr/bin/perl -w
    
    # Name
    #   smsgateway.pl
    #
    # Execution
    #   smsgateway.pl -f modem.cfg > /var/log/smsgatewayerror.log 2>&1 &
    #
    # License
    #   This program is copyleft. You have the right to freely use, modify,
    #   copy, and share software, works of art, etc., on the condition that
    #   these rights be granted to all subsequent users or owners. 
    #
    # Author
    #   Arief Yudhawarman <awarmanff@yahoo.com>
    #
    # Revision
    #   Mon, 8 Aug 2016
    #
    
    use strict;
    use Device::Modem;
    use Device::Gsm::Pdu;
    use POSIX qw(time strftime);
    use DBI;
    use vars qw(%opt);
    
    $SIG{__WARN__} = sub { warn sprintf("[%s] ", scalar localtime), @_ };
    $SIG{__DIE__}  = sub { die  sprintf("[%s] ", scalar localtime), @_ };
    $|=1;
    
    #
    # Statics
    #
    
    my %smsstatus = (
      # Short message transaction completed
      '00' => 'Short message received succesfully',
      '01' => 'Short message forwarded to the mobile phone, but unable to confirm delivery',
      '02' => 'Short message replaced by the service center',
      # Temporary error, Service center still trying to transfer SMS
      '20' => 'Congestion (SMSC still trying to transfer SMS)',
      '21' => 'SME busy (SMSC still trying to transfer SMS)',
      '22' => 'No response from SME (SMSC still trying to transfer SMS)',
      '23' => 'Service rejected (SMSC still trying to transfer SMS)',
      '24' => 'Quality of service not available (SMSC still trying to transfer SMS)',
      '25' => 'Error in SME (SMSC still trying to transfer SMS)',
      # Permanent error, Service center is not making any more transfer attempts
      '40' => 'Remote procedure error (SMSC no longer to transfer SMS)',
      '41' => 'Incompatible destination (SMSC no longer to transfer SMS)',
      '42' => 'Connection rejected by SME (SMSC no longer to transfer SMS)',
      '43' => 'Not obtainable (SMSC no longer to transfer SMS)',
      '44' => 'Quality of service not available (SMSC no longer to transfer SMS)',
      '45' => 'No interworking available (SMSC no longer to transfer SMS)',
      '46' => 'SM validity period expired (SMSC no longer to transfer SMS)',
      '47' => 'SM deleted by originating SME (SMSC no longer to transfer SMS)',
      '48' => 'SM deleted by service center administration (SMSC no longer to transfer SMS)',
      '49' => 'SM does not exist (SMSC no longer to transfer SMS)',
      # Temporary error, Service center is not making any more transfer attempts
      '60' => 'Congestion (SMSC no longer to transfer SMS)',
      '61' => 'SME busy (SMSC no longer to transfer SMS)',
      '62' => 'No response from SME (SMSC no longer to transfer SMS)',
      '63' => 'Service rejected (SMSC no longer to transfer SMS)',
      '64' => 'Quality of service not available (SMSC no longer to transfer SMS)',
      '65' => 'Error in SME (SMSC no longer to transfer SMS)',
    );
    
    my %pdutype = (
      '00' => 'SMS-DELIVER',
      '01' => 'SMS-SUBMIT',
      '10' => 'SMS-STATUS-REPORT',
      '11' => 'Reserved',
    );
    
    my $maxdig = 16;     # maximum digits of phone number
    my $maxchar = 160;   # maximum characters to send
    my $validity = 'A7'; # (A7 for 1 day, A8 for 2 days, A9 for 3 days)
    my $smscum = 0;
    my $is_cmgs = 'no';
    my $dayinsecond = 86400; # 1 day
    
    my ($SERIAL, $BAUD, $TIMEOUT, $LTIMEOUT, $ELAPSED, $CHECKTIME, $SMSTORESET, $DIR, $LOG, $PID, $PREFIX, $SMSDB);
    my ($modem, $atcmd);
    my ($dbh,$lastID);
    my $mdmAnswer;
    my ($resettime, $start, $elapse, $elapsed);
    
    #
    # Subs
    #
    
    sub getOptions()
    {
      use Getopt::Std;
      my $opt_string = 'hf:';
      getopts( "$opt_string", \%opt ) or usage();
      usage() if (!$opt{f});
      usage() if $opt{h};
    }
    
    sub ATSend {
      my ($atcmd) = @_;
      $modem->atsend("$atcmd");
      return $modem->answer();
    }
    
    sub usage()
    {
      print STDERR << "EOF";
    
    Usage: $0 [-h] -f file
      -h	  : this (help) message
      -f file : configuration file
    
    Example   : $0 -f modem.cfg
    
    EOF
      exit;
    }
    
    sub readConfig()
    {
      if ( open (IN, $opt{f})) {
        while (<IN>)
        {
          $SERIAL = $1 if (/^modem[\s]*=[\s]*(.*)/i);
          $BAUD  = $1 if (/^baud[\s]*=[\s]*(.*)/i);
          $TIMEOUT  = $1 if (/^timeout[\s]*=[\s]*(.*)/i);
          $LTIMEOUT  = $1 if (/^longtimeout[\s]*=[\s]*(.*)/i);
          $ELAPSED  = $1 if (/^elapsed[\s]*=[\s]*(.*)/i);
          $CHECKTIME  = $1 if (/^checktime[\s]*=[\s]*(.*)/i);
          $SMSTORESET = $1 if (/^smstoreset[\s]*=[\s]*(.*)/i);
          $DIR = $1 if (/^directory[\s]*=[\s]*(.*)/i);
          $LOG = $1 if (/^log[\s]*=[\s]*(.*)/i);
          $PID = $1 if (/^pid[\s]*=[\s]*(.*)/i);
          $PREFIX = $1 if (/^prefix[\s]*=[\s]*(.*)/i);
          $SMSDB = $1 if (/^database[\s]*=[\s]*(.*)/i);
        }
        close (IN);
      } else {
        print "Can not open file $opt{f} for reading: $!\n";
        exit 1;
      }
    }
    
    sub connectModem()
    {
      $modem = new Device::Modem( port => $SERIAL);
      if( $modem->connect( baudrate => $BAUD ) ) {
          print FLOG "\nModem is connected!\n\n";
      } else {
          print FLOG "\nSorry, no connection with serial port!\n";
          close FLOG;
          unlink $PID;
          exit 1;
      }
    }
    
    sub initModem()
    {
      $atcmd = "ATQ0 V1 E0 &C1 &D2 +FCLASS=0\n";
      print FLOG $atcmd . ATSend($atcmd) . "\n\n" ;
      sleep 1;
    
      $atcmd = "AT+IFC=2,2\n";
      print FLOG $atcmd . ATSend($atcmd) . "\n\n" ;
    
      $atcmd = "AT+CPIN?\n";
      print FLOG $atcmd . ATSend($atcmd) . "\n\n" ;
    
      $atcmd = "AT+CSQ\n";
      print FLOG $atcmd . ATSend($atcmd) . "\n\n" ;
    
      $atcmd = "AT+CREG?\n";
      print FLOG $atcmd . ATSend($atcmd) . "\n\n" ;
    
      $atcmd = "AT+CSMS=1\n";
      print FLOG $atcmd . ATSend($atcmd) . "\n\n" ;
    
      $atcmd = "AT+CNMI=1,2,0,1,0;+CMEE=1\n";
      print FLOG $atcmd . ATSend($atcmd) . "\n\n" ;
    
      $atcmd = "AT+CPMS=\"SM\"\n";
      print FLOG $atcmd . ATSend($atcmd) . "\n\n" ;
    
      $atcmd = "AT+CPMS?\n";
      print FLOG $atcmd . ATSend($atcmd) . "\n\n" ;
    
      $atcmd = "AT+CMGF=0\n";
      print FLOG $atcmd . ATSend($atcmd) . "\n\n" ;
    
      $atcmd = "AT+CSMP=1,167,0,0\n";
      print FLOG $atcmd . ATSend($atcmd) . "\n\n" ;
    }
    
    sub unpackTime {
      my ($hex) = @_;
      my $packed = pack('h14',$hex);
      my ($year,$month,$date,$hour,$minute,$second,$zone)=unpack('H2' x 7,$packed);
      $year+=2000;$zone=($zone*15)/60;
      return "$year-$month-$date $hour:$minute:$second";
    }
    
    sub unpackAddress {
      my ($hex, $ofs) = @_;
      $ofs += 2;
      my $adrslenhex = substr ($hex, $ofs, 2);
      my $adrslen = sprintf("%d",hex($adrslenhex));
      if ( ($adrslen % 2) > 0 )
      { $adrslen++; }
      $ofs += 2;
      my $adrstype = substr ($hex, $ofs, 2);
      $ofs += 2;
      my $addresspdu = $adrslenhex . $adrstype . substr ($hex, $ofs, $adrslen);
      my $address = Device::Gsm::Pdu::decode_address($addresspdu);
      if ($adrstype eq '81' )
      {
        $address = substr ($address,1);
        $address = $PREFIX . $address;
      } 
      return ($address, $adrslen+$ofs);  
    }
    
    sub decodePDU {
      my ($hex,$dcs) = @_;
      if ($dcs eq "04")
      {
        return ("The decoding is not available.","Alphabet 8bit");
      } elsif ($dcs eq "08") {
        return ("The decoding is not available.","UCS2(16)bit");
      } else {
        return (Device::Gsm::Pdu::pdu_to_latin1($hex),"SMS Default Alphabet");
      }
    }
    
    sub validity {
      my ($tpvp) = @_;
      my $val;
      if ($tpvp <= 143)
      {
        $val = ($tpvp + 1) * 5;
        return ($val,"minutes");
      } elsif ( ($tpvp >= 144) && ($tpvp<=167))
      {
        $val = 12*60 + ( ($tpvp-143) * 30);
        return ($val,"minutes");
      } elsif ( ($tpvp >= 168) && ($tpvp<=196))
      {
        $val = ($tpvp - 166) * 1;
        return ($val,"day(s)");
      } else {
        $val = ($tpvp - 192) * 1;
        return ($val,"week(s)");
      }
    }
    
    sub getID {
      my ($dest,$text) = @_;
      my $now = POSIX::strftime ("%Y-%m-%d %H:%M:%S", localtime);
      $text =~ s/'/''/gm;
      eval {
        $dbh->do("INSERT INTO smsout (destination,text,datetime) 
                  VALUES ('$dest','$text','$now')");
        $dbh->commit( );
        return $dbh->last_insert_id("", "", "smsout", "no");
      }
    }
    
    sub smsOut {
      my ($id,$ref, $dest, $text, $is_err, $tpscts) = @_;
      $text =~ s/'/''/gm;
      eval {
        $dbh->do("UPDATE smsout 
                 SET reference=$ref,destination='$dest',text='$text',
                     error='$is_err',tpscts='$tpscts' 
                 WHERE no=$id");
        $dbh->commit( );
      }
    }
    
    sub smsOutStat {
      my ($ref, $dest, $status, $tpdt) = @_;
      eval {
        $dbh->do("UPDATE smsout 
                  SET status='$status',tpdt='$tpdt' 
                  WHERE reference=$ref AND destination='$dest' and status is null");
        $dbh->commit( );
      }
    }
    
    sub smsIn {
      my ($from,$text,$tpscts) = @_;
      $from =~ s/\x00//g;
      $text =~ s/'/''/gm;
      eval {
        $dbh->do("INSERT INTO smsin (sender, text, tpscts) 
                  VALUES ('$from','$text','$tpscts')");
        $dbh->commit( );
      }
    }
    
    sub decodeSM {
      my ($is_err,$ref,$data) = @_;
      my $ofs = 0;
      my $smsc = substr ( $data, $ofs, 2);
      my ($tprp,$tpudhi,$tpsri,$tpsrq,$tpsrr,$tprd,$reserved1,$reserved2,$tpmms,$tpmti);
      my ($tpudl,$tpud,$tppid);
      my ($pduh,$binstr);
      my ($val,$len,$temp,$coding,$msgref);
      my ($tpdcs,$tpda,$now,$sms,$period);
      my ($tpst,$tpdt,$tpvp,$tpvpf,$tpmr);
      my ($tpoa,$tpscts,$tpra);
    
      $ofs += 2;
      if ($smsc ne "00" ) {
        $len = sprintf("%d",hex($smsc));
        $smsc = $smsc . substr ( $data, $ofs, $len*2);
        $ofs += $len*2;
        $smsc = Device::Gsm::Pdu::decode_address($smsc);
      }
      $pduh = substr ($data, $ofs, 2);
      $binstr = unpack('B8',chr(hex($pduh)));
      ($temp,$tpmti)=unpack("A6 A2" ,$binstr);
      if ($tpmti eq "00" )
      {
        # SMS-DELIVER
        ($tprp,$tpudhi,$tpsri,$reserved1,$reserved2,$tpmms)=unpack("A1 A1 A1 A1 A1 A1" ,$temp);
        ($tpoa, $ofs) = unpackAddress($data, $ofs);
        $tppid = substr ($data, $ofs, 2);
        $ofs += 2;
        $tpdcs = substr ($data, $ofs, 2);
        $ofs += 2;
        $tpscts = substr ($data, $ofs, 14);
        $ofs += 14;
        $tpudl = substr ($data, $ofs, 2);
        $ofs += 2;
        $tpud  = substr ($data, $ofs);
        ($sms,$coding) = decodePDU($tpud,$tpdcs);
        print FLOG "** Status Report **\n";
        print FLOG "PDU type   : $pdutype{$tpmti}\n";
        print FLOG "SMSC       : $smsc\n";
        print FLOG "From number: $tpoa\n";
        print FLOG "Time stamp : " . unpackTime($tpscts) . "\n";
        print FLOG "Message    : $sms\n";
        print FLOG "Data coding: $coding\n";
        print FLOG "\n";
        smsIn($tpoa,$sms,unpackTime($tpscts));
      } elsif ($tpmti eq "10" )
      {
        # SMS-STATUS REPORT
        ($reserved1,$tpsrq,$reserved2,$tpmms)=unpack("A2 A1 A2 A1" ,$temp);
        $ofs += 2;
        $tpmr = substr ($data, $ofs, 2);
        $msgref = sprintf("%d",hex($tpmr));
        ($tpra, $ofs) = unpackAddress($data, $ofs);
        $tpscts = substr ($data, $ofs, 14);
        $ofs += 14;
        $tpdt = substr ($data, $ofs, 14);
        $ofs += 14;
        $tpst = substr ($data, $ofs, 2);
        print FLOG "** Status Report **\n";
        print FLOG "Reference  : $msgref\n";
        print FLOG "PDU type   : $pdutype{$tpmti}\n";
        print FLOG "SMSC       : $smsc\n";
        print FLOG "From number: $tpra\n";
        print FLOG "Time stamp : " . unpackTime($tpscts) . "\n";
        print FLOG "Discharge  : " . unpackTime($tpdt) . "\n";
        print FLOG "Status     : $tpst ($smsstatus{$tpst})\n";
        print FLOG "\n";
        smsOutStat($msgref, $tpra, $tpst, unpackTime($tpdt));
      } else {
        # SMS-SUBMIT
        ($tprp,$tpudhi,$tpsrr,$tpvpf,$tprd)=unpack("A1 A1 A1 A2 A1" ,$temp);
        $ofs += 2;
        $tpmr = substr ($data, $ofs, 2);
        ($tpda, $ofs) = unpackAddress($data, $ofs);
        $tppid = substr ($data, $ofs, 2);
        $ofs += 2;
        $tpdcs = substr ($data, $ofs, 2);
        $ofs += 2;
        $tpvpf = sprintf ("%d", hex($tpvpf));
        ($val,$period) = ("Not present","");
        if ($tpvpf > 0) {
          $tpvp = substr ($data, $ofs, 2);
          $ofs += 2;
          ($val,$period) = validity(hex($tpvp));
        }
        $tpudl = substr ($data, $ofs, 2);
        $ofs += 2;
        $tpud = substr ($data, $ofs);
        ($sms,$coding) = decodePDU($tpud,$tpdcs);
        $now = POSIX::strftime ("%Y-%m-%d %H:%M:%S", localtime);
        if ($is_err eq " " ) {
          print FLOG "** Status Report **\n";
          print FLOG "Reference  : $ref\n";
          print FLOG "PDU type   : $pdutype{$tpmti}\n";
          print FLOG "SMSC       : $smsc\n";
          print FLOG "Recipient  : $tpda\n";
          print FLOG "Message    : $sms\n";
          print FLOG "Validity   : $val $period\n";
          print FLOG "Data coding: $coding\n";
          print FLOG "\n";
        }
        smsOut($lastID,$ref,$tpda,$sms,$is_err,$now);
      }
    }
    
    sub saveLog {
      my ($text) = @_;
      my $now = POSIX::strftime ("%Y-%m-%d %H:%M:%S", localtime);
      eval {
        $dbh->do("INSERT INTO log (text, datetime) 
                  VALUES ('$text','$now')");
        $dbh->commit( );
      }
    }
    
    sub closeApp() {
      print FLOG "\nResetting modem done.\n";
      print FLOG "Finishing at " . POSIX::strftime ("%a, %d-%m-%Y %T %Z", localtime) . ".\n";
      close FLOG;
      $modem->reset();
      $dbh->disconnect();
      unlink $PID;
      exit;
    }
    
    sub resetModem {
      my ($string) = @_;
      print FLOG "\n$string\n";
      $modem->reset();
      sleep 3;
      connectModem();
      initModem();
      $resettime = POSIX::time();
    }
    
    sub getResponse {
      my ($pdu) = @_;
      my $mdmAnswer = $modem->answer();
      my (@val,$val);
    
      if ($mdmAnswer) {
        $mdmAnswer =~ s/[\r\n\s]+/ /g; 
        @val = split(/ /,$mdmAnswer);
        if ($val[0] =~ /CMGS/) {
          print FLOG "$val[0] $val[1]\n\n";
          decodeSM(' ',$val[1],$pdu);
          $is_cmgs = 'yes';
    
        # SMS Error
        } elsif ($val[0] =~ /CMS/) { 
          print FLOG "$val[0] $val[1] $val[2]\n\n";
          decodeSM("$val[0] $val[1] $val[2]",0,$pdu);
          $is_cmgs = 'yes';
    
        # SMS-STATUS-REPORT
        } elsif ($val[0] =~ /CDS/) { 
          print FLOG "$val[0] $val[1] $val[2]\n\n";
          decodeSM(' ',0,$val[2]);
          $atcmd = "AT+CNMA\n";
          print FLOG $atcmd . ATSend($atcmd) . "\n\n";
    
        # SMS-DELIVER
        } elsif ($val[0] =~ /CMT/) { 
          print FLOG "$val[0] $val[1] $val[2]\n\n";
          decodeSM(' ',0,$val[2]);
          $atcmd = "AT+CNMA\n";
          print FLOG $atcmd . ATSend($atcmd) . "\n\n";
    
        # log unparseable response(s) such as RING, etc
        } else {
          print FLOG $mdmAnswer; print FLOG "\n\n";
          saveLog($mdmAnswer);
        }
      }
    }
    
    #
    # Main
    #
    
    getOptions();
    readConfig();
    
    if (-e $PID)
    {
      print "\nsmsgateway already running\n";
      exit 1;
    }
    
    open (FLOG,">>$LOG") || die "Can not open $LOG: $!";
    FLOG->autoflush(1);
    print FLOG "\nSmsgateway starting at ".POSIX::strftime("%a, %d-%m-%Y %T %Z", localtime).".\n";
    
    connectModem();
    initModem();
    
    open (OUT,">$PID") || die "Can not open $PID: $!";
    print OUT $$;
    close OUT;
    
    $SIG{'INT'} =  'closeApp';
    $SIG{'KILL'} = 'closeApp';
    $SIG{'TERM'} = 'closeApp';
    
    my $OCTS1 = '003100';
    my $OCTS2 = '0000'.$validity;
    
    $dbh = DBI->connect("dbi:SQLite:dbname=$SMSDB","","",
      {RaiseError => 1, AutoCommit => 0});
    
    $resettime = POSIX::time();
    $elapsed = 0;
    
    my (@hpSMS,@files,$file);
    my ($dest,$encdest,$text,$enctext,$pdu,$length,$response);
    while (1) {
      if ( ($elapsed % $ELAPSED == 0) ) {
        mkdir $DIR if (! -e $DIR);
        @files = <$DIR/*>;
        foreach (@files) {
          $file = $_;
          open IN,"<$file" || die "Can not open $file: $!\n";
          @hpSMS = ();
          while (<IN>) { 
            chomp;
            push (@hpSMS, $_);
          } 
          close IN;
          unlink $file;
    
          foreach $_ (@hpSMS) { 
            if ( $_ =~ /([\+\d]+)[\s]+(.*)/) {
              $dest = $1;
              $text = $2;
              $dest =~ /.{1,$maxdig}/;
              $dest = $&;
              $text =~ /.{1,$maxchar}/;
              $text = $&;
              print FLOG "To: $dest\nSMS: $text\n\n";
              $encdest = Device::Gsm::Pdu::encode_address($dest);
              $enctext = Device::Gsm::Pdu::encode_text7($text);
              $pdu = "$OCTS1"."$encdest"."$OCTS2"."$enctext";
              $length = length ($pdu)/2 - 1;
              $atcmd = "AT+CMGS=$length\n";
              $response = ATSend($atcmd);
              print FLOG $atcmd . $response;
              $atcmd = "$pdu\cZ";
              $lastID = getID($dest,$text);
              $is_cmgs = 'no';
              $modem->atsend("$atcmd");
              print FLOG $atcmd . "\n\n" ;
    
              $start = POSIX::time();
              $smscum++;
              if ( $smscum >= $SMSTORESET )
              {
                while ( (POSIX::time()-$start) < $LTIMEOUT) {
                  getResponse($pdu);
                }
                resetModem("Resetting modem at " . POSIX::strftime ("%a, %d-%m-%Y %T %Z", localtime) . " after $smscum sms.");
                $smscum = 0;
              } else {
                do {
                  while ( (POSIX::time()-$start) < $TIMEOUT) {
                    getResponse($pdu);
                  }
                  $start = POSIX::time();
                } until ($is_cmgs eq 'yes');
              }
            }
          }
        }
      }
      getResponse($pdu);
    
      $elapsed = POSIX::time() - $resettime;
      if ( ($elapsed % $CHECKTIME == 0) ) {
        $atcmd = "AT+CPMS?\n";
        print FLOG $atcmd . ATSend($atcmd) . "\n\n" ;
      }
      if ( ($elapsed >= $dayinsecond) )
      {
        resetModem ("Resetting modem at " . POSIX::strftime ("%a, %d-%m-%Y %T %Z", localtime) . " after " . $elapsed/60 . " minutes");
      }
    }
    
  8. Kemudian buat file konfigurasi /usr/local/bin/modem.cfg dengan text editor. Sesuaikan port modem yang digunakan apakah serial (/dev/ttyS0) atau usb (/dev/ttyUSB0).
    modem	= /dev/ttyUSB0
    baud	= 115200
    # waiting for modem response after send one sms
    timeout	= 30
    # waiting for modem response after send some sms (smstoreset)
    longtimeout = 300
    # elapsed time for reading sms file
    elapsed = 10
    # check the modem after some seconds
    checktime = 900
    # reset modem after sending some sms
    smstoreset = 30
    # where sms file is keep
    directory = /var/tmp/sms
    # log file
    log = /var/log/smsgateway.log
    # pid file
    pid = /var/run/smsgateway.pid
    # international prefix number for indonesia
    prefix = +62
    # database using sqlite
    database = /var/log/sms.db
    
  9. Script smsgateway bisa dieksekusi langsung di console dengan perintah

    /usr/local/sbin/smsgateway.pl -f /usr/local/bin/modem.cfg > /var/log/smsgateway.err 2>&1 &


    Alternatif lain adalah dengan membuat script init smsgateway untuk menghidupkan aplikasi smsgateway. Kelebihan script ini adalah bisa mengetahui status smsgateway, tes kirim sms dan restart aplikasi dengan mudah. Simpan script di /etc/init.d dan set file permission ke 755.

    #!/bin/bash
    #
    # smsgateway  This shell script takes care of starting and stopping smsgateway
    #
    
    pid="/var/run/smsgateway.pid"
    cfg="/usr/local/sbin/modem.cfg"
    prog="/usr/local/sbin/smsgateway.pl"
    error="/var/log/smsgateway.err"
    smsdir="/var/tmp/sms"
    
    if ! [ -x $prog ]; then
            exit 0
    fi
    
    case "$1" in
      start) 
        if [ -f $pid ]; then
          echo "smsgateway is still running."
        else
          echo -n "Starting smsgateway:"
          $prog -f $cfg > $error 2>&1 &
          echo "."
          sleep 2
        fi
        ;;
      stop)
        if [ -f $pid ]; then
          echo -n "Stopping smsgateway:"
          read ppid < $pid
          /bin/kill $ppid
          echo "."
          sleep 2
        else
          echo "smsgateway is not running."
        fi
        ;;
      stat)
        if [ -f $pid ]; then
          echo "smsgateway is running."
        else
          echo "smsgateway is not running."
        fi
        ;;
      test)
        if [ -f $pid ]; then
          echo -n "Recipient : "; read RECIPIENT
          echo -n "Text      : "; read TEXT
          TMPFILE=`mktemp $smsdir/sms.XXXXXXXX`
          echo "$RECIPIENT $TEXT" > $TMPFILE
          echo "Sending SMS ..."
        else
          echo "smsgateway is not running."
        fi
        ;;
       *)
        echo "Usage: $0 {start|stop|stat|test}"
        exit 1
        ;;
    esac
    
    exit 0
    
  10. Buat direktori untuk menyimpan file sms sementara.

    mkdir /var/tmp/sms


Eksekusi smsgateway

Jalankan script init smsgateway sebagai user root.

[root@server ~]# /etc/init.d/smsgateway stat
smsgateway is not running.
[root@server ~]# /etc/init.d/smsgateway start
Starting smsgateway:.
[root@server ~]# /etc/init.d/smsgateway stat
smsgateway is running.


Isi file /var/log/smsgateway.log setelah eksekusi smsgateway.

Smsgateway starting at Fri, 07-11-2014 16:23:23 WIT.

Modem is connected!

ATQ0 V1 E0 &C1 &D2 +FCLASS=0
OK

AT+IFC=2,2
OK

AT+CPIN?
+CPIN: READY

AT+CSQ
+CSQ: 24,5

OK

AT+CREG?
+CREG: 0,1

OK

AT+CSMS=1
+CSMS: 1,1,1

OK

AT+CNMI=1,2,0,1,0;+CMEE=1
OK

AT+CPMS="SM"
+CPMS: 0,10,0,10

OK

AT+CPMS?
+CPMS: "SM",0,10,"SM",0,10

OK

AT+CMGF=0
OK

AT+CSMP=1,167,0,0
OK

Untuk mengetahui teknis perintah AT Command di atas silahkan merujuk ke link referensi pada akhir tulisan ini.

Tes kirim sms.

[root@server ~]# /etc/init.d/smsgateway test 
Recipient : 085236006679
Text      : Testing
Sending SMS ...
[root@server ~]# 


Isi file /var/log/smsgateway.log setelah sms masuk ke nomor tujuan.

To: 085236006679
SMS: Testing

AT+CMGS=21
> 0031000C818025630066970000A707D4F29C9E769F01

+CMGS: 54

** Status Report **
Reference  : 54
PDU type   : SMS-SUBMIT
SMSC       : 00
Recipient  : +6285236006679
Message    : Testing@
Validity   : 1440 minutes
Data coding: SMS Default Alphabet

+CDS: 25 0006360C81802563006697411170617285824111706182708200

** Status Report **
Reference  : 54
PDU type   : SMS-STATUS-REPORT
SMSC       : 00
From number: +6285236006679
Time stamp : 2014-11-07 16:27:58
Discharge  : 2014-11-07 16:28:07
Status     : 00 (Short message received succesfully)

AT+CNMA
OK


Isi table smsout di file database /var/log/sms.db:

[yudi@server ~]$ sqlite3 /var/log/sms.db 
SQLite version 3.3.6
Enter ".help" for instructions
sqlite> select * from smsout;
1|54|+6285236006679|Testing@| |00|2014-11-07 16:27:52|2014-11-07 16:27:59|2014-11-07 16:28:07
sqlite> .quit

Kolom ke-6 yang berisi string 00 menunjukkan Short message received succesfully.

Kita coba kirim sms dengan pesan Halo apa kabar? ke sms gateway.
Isi file /var/log/smsgateway.log setelah pesan masuk diterima oleh smsgateway..

+CMT: ,34 06912618010000240D91265832066076F900004111707182052310C830FB0D0AC3C3A075581C96FF40

** Status Report **
PDU type   : SMS-DELIVER
SMSC       : +6281100000
From number: +6285236006679
Time stamp : 2014-11-07 17:28:50
Message    : Halo apa kabar? 
Data coding: SMS Default Alphabet

AT+CNMA
OK


Isi table smsin di file database /var/log/sms.db:

[yudi@server ~]$ sqlite3 /var/log/sms.db 
SQLite version 3.3.6
Enter ".help" for instructions
sqlite> select * from smsin;
1|+6285236006679|Halo apa kabar? |2014-11-07 17:28:50
sqlite> .quit


SMS Delivery Status

Jika sms berhasil dikirim nilai status yang diterima adalah ‘0’ atau ’00’. Yang kerap dijumpai adalah status ’45’ jika nomor yang dihubungi sudah expired atau ’46’ jika batas waktu pengiriman sms telah melampui validity report. Untuk arti dan maksud nilai status yang lain bisa dibaca pada tabel di bawah:

Status Description
0 Short message received succesfully
1 Short message forwarded to the mobile phone, but unable to confirm delivery
2 Short message replaced by the service center
20 Congestion (SMSC still trying to transfer SMS)
21 SME busy (SMSC still trying to transfer SMS)
22 No response from SME (SMSC still trying to transfer SMS)
23 Service rejected (SMSC still trying to transfer SMS)
24 Quality of service not available (SMSC still trying to transfer SMS)
25 Error in SME (SMSC still trying to transfer SMS)
40 Remote procedure error (SMSC no longer to transfer SMS)
41 Incompatible destination (SMSC no longer to transfer SMS)
42 Connection rejected by SME (SMSC no longer to transfer SMS)
43 Not obtainable (SMSC no longer to transfer SMS)
44 Quality of service not available (SMSC no longer to transfer SMS)
45 No interworking available (SMSC no longer to transfer SMS)
46 SM validity period expired (SMSC no longer to transfer SMS)
47 SM deleted by originating SME (SMSC no longer to transfer SMS)
48 SM deleted by service center administration (SMSC no longer to transfer SMS)
49 SM does not exist (SMSC no longer to transfer SMS)
60 Congestion (SMSC no longer to transfer SMS)
61 SME busy (SMSC no longer to transfer SMS)
62 No response from SME (SMSC no longer to transfer SMS)
63 Service rejected (SMSC no longer to transfer SMS)
64 Quality of service not available (SMSC no longer to transfer SMS)
65 Error in SME (SMSC no longer to transfer SMS)


CMS Error

CMS Error yang muncul di /var/log/smsgateway.log.

To: 081350424300
SMS: DESEMBER MUSIM LIBURAN - Bukan jaminan kita terbebas dari kecelakaan. Tetap Waspada dan Fokus dalam Bekerja.

AT+CMGS=108
> 0001000C8180310524340000006CC4E2B4D81416A5A066759A6C82984961551A74825A20617D1D7683D4E176DA1D7683D6697A18442FCBC56571780E2287E569D0BA3C2EB3C3EB70D8ED0251CBF4301C740DCFE1617218440EBB41C6F7BA3E0791C3EC701B242CAFCB7275D805^Z

+CMS ERROR: 512


Error ini akan disimpan di table smsout.

sqlite> select * from smsout where error like '%CMS%';
71|0|+6281350424300|DESEMBER MUSIM LIBURAN - Bukan jaminan kita terbebas dari kecelakaan. Tetap Waspada dan Fokus dalam Bekerja.|+CMS ERROR: 512||2016-02-23 11:41:17|2016-02-23 11:41:18|
sqlite> .quit

Kita bisa ulangi kembali mengirim sms ke nomor tujuan yang memberikan CMS ERROR di atas.


Log response modem yang tidak dikenal

Bahkan aplikasi smsgateway bisa menyimpan telpon masuk (RING) namun caller id tidak ikut tersimpan.

sqlite> select * from log;
1|RING|2016-02-24 12:58:22
2|RING|2016-02-24 12:58:25
3|RING|2016-02-24 12:58:28
4|RING|2016-02-24 12:58:31
5|RING|2016-02-24 12:58:34
6|RING|2016-02-24 12:58:37
7|RING|2016-02-24 12:58:51
8|RING|2016-02-24 12:58:54
9|RING|2016-02-24 12:58:57
10| 25 0006A70C81803519954508618040418400826180404194008200|2016-02-24 14:49:27
11|06B30C81802505442690612032814011826120328140228200|2016-02-23 18:00:00
12|A40C81803584889431618061907000826180610162008200|2016-02-24 15:26:59
sqlite> .quit



SMS Bulk

Selain mengirim sms melalui script smsgateway kita bisa mengirimkan sms bulk dengan mudah. Misal ada file csv – phonebook.csv – berisi nama dan nomor kontak yang dipisahkan oleh karakter “;”.

Amir;085236000001
Budi;085236000002
Danu;085236000003
Elya;085236000004
Nani;085236000005

Maka untuk kirim sms dengan isi pesan Tes kirim sms bulk perintahnya seperti di bawah ini:


cut -f2 phonebook.csv -d';'|while read NO; do echo $NO Tes kirim sms bulk.; done > /var/tmp/sms/sms.bulk

Format file sms adalah nomor telpon selular lalu spasi dan isi pesan. Isi pesan yang lebih dari 160 karakter akan dipotong otomatis.


Statistik

Contoh sms bulk yang dikirimkan pada tanggal 16 Agustus 2016. Ada 749 sms yang dikirimkan, 677 sms telah sampai ke tujuan dengan selamat, sisanya pending (status masih null) ada 65, failed ada 2 dan error ada 5.

[yudi@server ~]$ sqlite3 /var/log/sms.db 
SQLite version 3.3.6
Enter ".help" for instructions
sqlite> select count(*) from smsout where datetime>'2016-08-16';
749
sqlite> select count(*) from smsout where datetime>'2016-08-16' and status='00';
677
sqlite> select count(*) from smsout where datetime>'2016-08-16' and status is null and error =' ';
65
sqlite> select count(*) from smsout where datetime>'2016-08-16' and status <> '00' and error =' ';
2
sqlite> select no,reference,destination,status,error from smsout where datetime>'2016-08-16' and status <> '00' and error =' ';
12428|224|+628124107xxxx|45| 
13003|28|+628225560xxxx|45| 
sqlite> select count(*) from smsout where datetime>'2016-08-16' and status is null and error <> ' ';
5
sqlite> select no,reference,destination,status,error from smsout where datetime>'2016-08-16' and status is null and error <> ' ';
12730|0|+628524545xxxx||+CMS ERROR: 512
12813|0|+628534678xxxx||+CMS ERROR: 512
12815|0|+628534680xxxx||+CMS ERROR: 512
13071|0|+628565102xxxx||+CMS ERROR: 512
13089|0|+628331||+CMS ERROR: 38
sqlite> select 677/749.*100;
90.3871829105474

Keberhasilannya 90.38%🙂. Menurut pengalaman penulis kadang kala terjadi delivery status tidak dikirimkan oleh SMSC meski sms sampai ke nomor tujuan dan dibaca oleh yang bersangkutan. Jadi keberhasilannya tentu lebih dari 90.38%.


Kontribusi

Apabila anda merasa terbantu dengan tulisan ini anda bisa mengirimkan donasi sebesar Rp 100.000,- (seratus ribu rupiah) kepada norek BCA 0240253992 atau Mandiri 117-00-0642993-0 atas nama Arief Yudhawarman. Pengirim akan memperoleh script smsgateway.pl dengan tambahan modul untuk parsing unknown modem response untuk memprediksi SMS-STATUS-REPORT atau SMS-DELIVER plus script init smsgateway dengan modul untuk membaca ulang konfigurasi modem.cfg tanpa memerlukan restart smsgateway serta tidak ketinggalan pula update aplikasi ini.

Referensi:

  1. SMS PDU Mode
  2. SMS PDU formats demystified
  3. Send SMS using AT commands
  4. AT Commands For GSM/GPRS Wireless Modems
  5. How to Send and Receive SMS using GSM Modem
  6. SMS Status Report
  7. Online PDU Encoder and Decoder


Last update: 2018-08-18 21:41 +07:00

Written by awarmanf

August 18, 2016 at 2:42 pm

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: