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