Pull to refresh

Легкий PERL-скрипт для отправки и приема SMS через мобильный телефон

Reading time 8 min
Views 7.4K

Преамбула



В процессе самостоятельной разработки «Умного дома» ( далее по тексту УД) периодически возникала необходимость в написании небольших, но очень нужных утилит-программ-скриптов, в том или ином виде уже написанных и доступных на просторах интернет, но по тем или иным причинам непригодных для использования в проекте. «Почему непригодных?», спросит опытный Разработчик, может я просто «Не люблю кошек, потому, что не умею их готовить?». Может оно и так, но, тем не менее, когда стоит задача развернуться на чем-то вроде D-Link DIR-320, тут уж «не до жиру, быть бы живу». Тут, наверное, следует заметить, что ранее основным языком разработки УД был выбран PERL, как наиболее подходящий для «fast-and-dirty» разработки на целевой платформе. Почему «fast-and-dirty»? Все очень просто: никакая дистрибуция продукта не планировалась, разве что установка на пару-тройку устройств для друзей и родственников. Важно, что составляющая «fast» превалировала, так как до зимы оставалось пару месяцев, а система готовилась к установке на отапливаемой даче с проживанием только по выходным. Это означает, что остановка системы отопления в отстсутствии хозяев могла привести к самым плачевным последствиям, как то размораживание системы отопления.
Да, я знаю, что уже давно есть gnokii, CPAN SMS-Server-Tools и да и вообще много-чего. Однако после длительного перебора нескольких готовых решений, пришлось все-же приступать к изобретению велосипеда сызнова. Здесь я прекрасно понимаю, что лучше было бы вложить затраченное на написание скрипта время в разработку основной логики, но тем не менее факт остается фактом: рассмотренные готовые решения по тем или иным причинам не вписались в перечень требований. Об этом ниже.

Постановка задачи


Итак, поставлена задача общаться с главным управляющим процессом УД посредством SMS через мобильный телефон, непосредственно подключенный к хосту. Было решено, что «велосипед» должен отвечать следующим требованиям:
— минимум зависимостей от внешних библиотек;
— «fast-and-dirty» разработка (2 дня с первичной отладкой, больше времени не было);
— возможность работать в асинхронном по отношению к клиенту (главный процесс УД) режиме. Задержки и подвисания в обмене с телефоном не должны блокировать клиентский процесс. Никак.
— отсутсвие необходимости компиляции, что упрощает перенос на другие платформы, в первую очередь типа «embedded-linux»;
— минимальный функционал: приемка SMS+отправка SMS;
— простой интерфейс в духе unix;
— возможность работать в как режиме daemon, так и в режиме однокрантого запуска;
— поддержка SMS кириллицей.
— возможность работы с наибольшим количеством моделей телефонов, в.ч. старых.

Решение


Собственно, после постановки задачи круг поиска возможных решений сузился значительно. Было решено, что:
— это будет Perl — скрипт, поскольку perl уже имелся и работал достаточно сносно. Конечно, задача реализуема и на shell, но тогда терялось «fast».
— интерфейс к скрипту файловый, и только файловый. Это дает абсолютную асинхронность, правда в обмен на надежность.
— общение с телефоном будет происходить в режиме UDP. Это единственный способ реализовать два последних требования из предыдущего списка.
— скрипт будет максимально линейным, никакого ООП и даже модульности, разве что легкая процедурность в угоду читабельности и переиспользованию, буде такое потребуется. Одним словом «fast-and-dirty».

Что получилось


Собственно, около трехсот строк скрипта и использование в качестве внешней библиотеки CPAN модуля Device::SerialPort. Последнее обстоятельство значительно ухудшает переносимость, т.к. данный модуль требует компиляции (скорее всего, кросс-компиляции, т.к. нативно не скомпилировался), однако он доступен готовым в большинстве репозиториев. В целом задача реализована, скрипт проработал уже пару лет без каких-либо проблем. Использовался с телефоном Siemens S65, с другими не приходилось. Для работы скрипта необходимы еще два файла данных: UCS.map и UCS.unmap, которые достаточно бессодержательны и обьемны для публикации здесь, однако с удовольствием поделюсь с желающими их получить. За подсказку как их выложить на Хабр будучи read-only буду весьма признателен (не ирония).

Вкратце о скрипте: все наиболее важные константы (пути, режим логирования и т.п) обьявлены и инициализированы в начале, дальше править практически нечего. Дя отправки файл сообщения нужно выложить в директорий $msgdir. Имя файла должно соответствовать конвенции $outgoingfilemask.$date.$MSISDN,
где $outgoingfilemask — значение маски из шапки скрипта, $date — дата отправки (плохой вариант) или секвентальный номер, используется во избежание наложения имен файлов при пакетной отправке, $MSISDN — номер телефона получателя в международном формате, но без всяких префиксов типа 810, 00, + и т.п. В самом файле — текст сообщения транслитом. Почему транслитом — так получилось («fast&dirty»). На самом деле это легко чинится за счет перезаполнения файлов UCS.map и UCS.unmap под практически любую кодировку.
Прием работает аналогично, но с маской имени файлов $incomingfilemask. Обе маски можно переопределить, но они обязательно должны содержать точку (доп. защита). Все принимаемые сообщения скрипт удаляет из памяти телефона, телефон должен быть настроен на прием СМС в основную память (не на SIM-карту). Копии сообщений могут сохраняться в $backupdir при установленном флаге $backup. Кажется все.

#!/opt/bin/perl

use Device::SerialPort;

#script global setting values

my $port_path = "/dev/usb/tts/0";
my $basedir 		= '/opt/files/';
my $msgdir 		= '/opt/files/msg/';
my $logdir		= '/opt/files/log/';
my $backupdir 		= '/opt/files/msg_backup/';
#my $backup 		= 1;
my $log  			= 1;
my $incomingfilemask 	= "in.msg";
my $outgoingfilemask 	= "out.msg";
my $sleeptime		= 10; # intercycle sleep time in seconds if no new messages
my $runcycles 		= 100;  #if runcycles set more than 1000, script will run endless

if($log ==1){
 open (LOG,">>$logdir".'UDP.log')
}else{
 open (LOG,">/dev/null");
}

my $run = 1;

# serial port init section

$port = new Device::SerialPort($port_path) or die "cannot open serial port:$!\n";
$port->baudrate(115200); #for siemens
#$port->baudrate(921600);
$port->parity("none");
$port->databits(8);
$port->stopbits(1);
$port->read_char_time(0);
$port->read_const_time(1000);

my %map;
my %unmap;
chdir ($basedir);
loadMAP();

# init mobile phone

send_at('AT+CMGF=0'); #set to receive unread messages
send_at('AT+CPMS="ME"'); #set default storage to mobile

# main cycle section

while($run){
  my @msg2del;
  my $rcvd_id = 0;
  my @mobile_out = split("\r",talk_mobile("AT+CMGL=4\r\n"));
  #print join ("\n",@mobile_out)."\n";
  my $totalsize = 0;
  foreach $line ( @mobile_out){
   $line =~ s/\n//g;
   
   if($line eq ''){ next};
   if($line =~ /\+CMGL:/){
    #print "AT response: $line\n";
    ($header,$param) = split(/:/,$line);
    (@id) = split(/,/,$param);
    #print "msg id's:".join(':',@id)."\n";
    $msg2del[$rcvd_id]= $id[0];
    $rcvd_id ++;
    next;
   }
    
   $totalsize +=length($line);
   if($line =~/07/){
    #print LOG $line."\n";
    my $parserpos = 0;
    my $LoSMSC = hex(substr($line,$parserpos,2));
    $parserpos = $LoSMSC*2+4; # jump over SMSC address
    my $LoMSISDN = hex(substr($line,$parserpos,2));
    $parserpos += 2;
    my $toMSISDN = substr($line,$parserpos,2);
    $parserpos += 2;
    unless(int($LoMSISDN/2)*2 == $LoMSISDN){
     $LoMSISDN ++;
    }
    $senderMSISDN = unpack_number(substr($line,$parserpos,$LoMSISDN));
    $parserpos += $LoMSISDN+2; ## jump protocol identifier
    my $TP_DCS = hex(substr($line,$parserpos,2));
    $parserpos +=2;
    my $TP_SCTS = swap_number(substr($line,$parserpos,14));
    $parserpos +=14;
    my @TS = split('',$TP_SCTS);
    my $rcvd_date = $TS[4].$TS[5].$TS[2].$TS[3].'20'.$TS[0].$TS[1];
    my $rcvd_time = $TS[6].$TS[7].$TS[8].$TS[9].$TS[10].$TS[11];
    my $TP_UDL = hex(substr($line,$parserpos,2));
    $parserpos +=2;
    my $msg_text = hex2ascii(substr($line,$parserpos,$TP_UDL*2));
    my $msgfilename = "$msgdir/$incomingfilemask.$rcvd_date$rcvd_time.$senderMSISDN";
    open (OUT,">$msgfilename") || print "cannot create $msgfilename";
    #print   "LoMSISDN=$LoMSISDN,MSISDN=$senderMSISDN,DCS=$TP_DCS,date=$rcvd_date $rcvd_time, msglen=$TP_UDL, pos=$parserpos\n";
    print OUT $msg_text;
    close (OUT);
  }else{
   #print "tag not found for $line\n";
  }
 }
 while($rcvd_id >0){
  my $msgid = $msg2del[$rcvd_id-1];
  send_at('AT+CMGD='.$msgid)."\n";
  $rcvd_id --;
 }
 #send outgong messages section
 if(opendir(MSGDIR,$msgdir)){
  my @files = readdir(MSGDIR);
  foreach $msgfile (@files){
   if($msgfile =~ /$outgoingfilemask/){
    #print "processing outgoing message $msgfile...";
    ($mask,$mask2,$date,$MSISDN) = split(/\./,$msgfile);
    if(open(MSGIN,$msgdir.$msgfile)){
     while(<MSGIN>){
      chomp;
      #reading text of the message
      if(defined($_)){send_SMS($MSISDN,$_)};
      #print "to $MSISDN,text <$_>\n";
     }
     close(MSGIN);
     system "rm $msgdir$msgfile";
    }else {print  "cannot open msg file $msgfile"};
   }
  }
  closedir(MSGDIR);
 }else{
  print LOG "cannot open outgoing message directory $msg_input_dir\n"; 
 }
 
 if($runcycles ==0 ){
  undef $run;
 }else{
  unless($runcycles >= 1000){$runcycles --;};
  sleep($sleeptime); 
 }
}
close(LOG);

sub send_SMS{
 my ($MSISDN,$text) = @_;
 $TP_LOA = sprintf("%02X",length($MSISDN));
 $TP_MSISDN = unpack_number($MSISDN);
 $TP_UD = ascii2hex($text);
 $TP_UDL = sprintf("%02X",length($TP_UD)/2);
 $TP_TOA = '91'; #international number
 $outline = $TP_SMSC.'1100'.$TP_LOA.$TP_TOA.$TP_MSISDN.'0008AA'.$TP_UDL.$TP_UD;
 $lout = length($outline)/2;
 $outline = '00'.$outline;
 if(defined($backup)){
 
  if(open(BF,">$backupdir/$MSISDN.".int(1000*rand()))){
   print BF $outline;
   close(BF);
  }
 }
 my @mobile_out = split("\r",talk_mobile("AT+CMGS=$lout\r\n"));
 my $totalsize = 0;
 my $prompt;
 foreach $line ( @mobile_out){
  $line =~ s/\n//g;
  if($line =~ /\>/){$prompt = 1};
 };
 if($prompt ==1){
  my @mobile_out = split("\r",talk_mobile($outline.chr(26)));
  my $totalsize = 0;
  foreach $line ( @mobile_out){
   $line =~ s/\n//g;
  };
 }else{
  #no prompt from mobile - error
 }
}


sub hex2ascii{
 
 my $inline = shift;
 my $lol = length($inline);
 #print "inline=<".$inline.">\n";
 my $result = '';
 my $seek = 0;
 while(defined($quad = substr($inline,$seek*4,4))){
  $result .= $map{$quad};
  $seek ++;
 }
 #print "length =$lol,$seek characters converted\n";
 return $result;
}

sub ascii2hex{
 my $inline = shift;
 my $result = '';
 my $seek = 0;
 while(defined($char = substr($inline,$seek,1))){
  
  unless($char eq ''){$result .= $unmap{$char}};
  $seek ++;
 }
 return $result;

}

sub unpack_number{
 my $result = '';
 my $inline = shift;
 my $seek =0;
 $num_len = length($inline);
 if(($num_len - 2 * int($num_len / 2)) >0 ){
  $inline .='F';
 }
 while($pair = substr($inline,$seek*2,2)){
  $result .= reverse($pair);
  $seek ++; 
 }
 return ($result);
}


sub swap_number{
 my $result = '';
 my $inline = shift;
 my $seek =0;
 while($pair = substr($inline,$seek*2,2)){
 $result .= reverse($pair);
 $seek ++; 
 }
 return ($result);
}

sub send_at{
 my $cmd = shift;
 my $result;
 my @mobile_out = split("\r",talk_mobile($cmd."\r\n"));
 my $totalsize = 0;
 foreach $line ( @mobile_out){
  $line =~ s/\n//g;
  if($line eq 'OK'){
  $result = 1;
  }elsif($line eq 'ERROR'){
  $result = -1;
  }
 }
}

sub talk_mobile{
 my $cmd = shift;
 $port->lookclear;
 $port->write("$cmd");

 my $read_chars = 0;
 my $buffer = "";
 my $eol =1;
 while($eol){
   my ($count,$saw) = $port->read(255);
   if($count > 0){
    $buffer.= $saw;
    if(($saw =~ /OK/)or($saw =~/ERROR/)or($saw =~/\>/)) {undef $eol}
  }
 }  
 return $buffer;
}


sub loadMAP{
 
 if(open(UCS,"UCS.map")){
  $loaded = 0;
  while(<UCS>){
   chomp;
   my ($code,$value)=split(/,/,$_,2);
   $map{$code} = $value;
   $loaded ++;
  }
  #print $loaded ." patterns loaded\n";  
  close(UCS);
 }else{ print "cannot open UCS.map file\n"};

 if(open(UUCS,"UCS.unmap")){
  $loaded = 0;
  while(<UUCS>){
   chomp;
   my ($code,$value)=split(/,/,$_,2);
   $unmap{$code} = $value;
   $loaded ++;
  }
  close(UUCS);
 }else{ print "cannot open UCS.unmap file\n"};


Полезные ссылки:
CPAN Serial Port Module
howToReceiveSMSUsingPC
Tags:
Hubs:
+11
Comments 17
Comments Comments 17

Articles