Преамбула
В процессе самостоятельной разработки «Умного дома» ( далее по тексту УД) периодически возникала необходимость в написании небольших, но очень нужных утилит-программ-скриптов, в том или ином виде уже написанных и доступных на просторах интернет, но по тем или иным причинам непригодных для использования в проекте. «Почему непригодных?», спросит опытный Разработчик, может я просто «Не люблю кошек, потому, что не умею их готовить?». Может оно и так, но, тем не менее, когда стоит задача развернуться на чем-то вроде 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