Предыстория
Однажды возник вопрос о том, как оперативно получать информацию о состоянии сети, когда находишься не у рабочего места.
Путем нехитрых размышлений был найден единственный выход: отсылка SMS-сообщений системой мониторинга о ключевых событиях.
Для реализации этого проекта были куплены GSM модем Siemens MC35i и 1-портовый асинхронный сервер Moxa NPort-5110, обеспечивающий преобразование интерфейса RS-232 в Ethernet и конечно же сервер под FreeBSD 🙂
Moxa NPort-5110 был приобретен для цели отсылки SMS-сообщений из любого сегмента сети, а не только с машины, к которой подключен GSM-шлюз.
Теория с примерами
Итак, конфигурация схемы следующая:
GSM модем включен в Moxa NPort-5110 кабелем RS-232.
Moxa имеет адрес 10.100.0.2:4001. Для отправки сообщения на латинице нужно выполнить следующую последовательность команд на модеме:
telnet 10.100.0.2 4001 AT+CPIN=7256 OK AT+CMGF=1 OK AT+CMGS="+79101234567" >test,test,testCTRL+Z +CMGS
где CTRL+Z — комбинация клавиш, нажатие которой означает конец сообщения.
С отправкой сообщения «Ахтунг!» на кириллице на номер +79101234567 все сильно сложнее, т.к. сообщение должно уходить пакетом в формате PDU в кодировке UCS2 (юникодная кодировка, поддерживающая в том числе кириллицу), а потому рассмотрим этот случай подробнее.
Пример:
telnet 10.100.0.2 4001 AT+CPIN=7256 OK AT+CMGF=0 OK AT+CMGS=28 >0011000B919701214365F70018C10E0410044504420443043D04330021CTRL+Z +CMGS
Разберем поподробнее эту «кашу»:
AT+CMGF=0 - установка модема в PDU-режим AT+CMGS=28 - длина строки пакета /2 -1 0011000B919701214365F70018C10E0410044504420443043D04330021 - сам PDU пакет
Формат PDU-пакета представляет собой 16-ричную последовательность, передающуюся человекочитаемой строкой (не ASCII-представления самих 16-ричных чисел). Оригинал описания (сохраненная копия) формата PDU пакета (на английском).
Перевод и коментарии:
00 Длина и номер SMS-центра провайдера. у нас — дефолтный из настроек GSM
11 Сообщение SMS-Submit (сохраненная копия)
0B Длина телефонного номера получателя (количество цифр в нем — в нашем случае 11)
91 Тип телефонного номера получателя (у нас получается «международный тип с планом нумерации Е.164/E.163) (сохраненная копия)
6 байт телефонный номер получателя (кодировка описана ниже)
00 Протокол (00 — SMS) (сохраненная копия)
18 Кодирование данных (08=UCS2,00-latin 7 bit etc. Старший байт — не сохранять в истории получателя) (сохраненная копия)
С1 Доставка актуальна 1 неделю (как рассчитывается) (сохраненная копия)
0E Длина сообщения («байт». длина символов кодированной UCS2-строки/2)
xx байт Кодированное сообщение (текст в UCS2, 16-ричное представление)
по-порядку подробней:
- первый байт на некоторых телефонах передавать не следует (работа возможна только через SMS-центр провайдера, телефон другого не умеет)
- номер получателя (алгоритм создавали наркаманы). кодируется «как есть» путем переставления местами соседних цифр, «добивается» до длины в 12 символов символами «F». например номер доблестной советской милиции (02 кто забыл) кодируется как 20FFFFFFFFFF, а номер из нашего примера (+79101234567) — как 9701214365F7. З.Ы. Не забывайте что длина SMS-сообщения ограничена, в кодировке UCS2 70-ю символами, на латинице — 160-ю символами.
Кодирование отправки SMS сообщений на PHP
<?
//Массив с параметрами GSM шлюза
$init=array('ip'=>"10.100.0.2",'port'=>'4001','pin'=>'7256');
// Отправка СМС :)
send_sms("Это тест","9101234567");
//Функция отсылки СМС
//$mess - сообщение
//$mob - номер мобильного в формате ХХХУУУУУУУ
//$debug - 1 - выводить отладочную инфу, 0 - нет
function send_sms($mess,$mob,$debug=0){ global $init; //Если сообщение и номер мобильника не пустые. //Проверку правильности формата мобильника возлагается на вас ;-) if ($mess && $mob){ $len=strlen($mess); $mob="7".$mob; $ooo=""; if (preg_match('/^([x0Ax0Dx20-x7F]+)$/',$mess,$tmp)){ if ($len<=160){ $len=0; print "8-bit encoded SMS<br>"; }else{ print "Длина сообщения в 8-битной кодировке [$len] больше 160 символов!<br>n"; return -1; } }else{ if ($len<=70){ $mess=cp1251_2ucs2($mess); $ret.="00";//it is only an indicator of the length of the SMSC information supplied (0) $ret.="11"; //First octet of the SMS-SUBMIT message. $ret.="00"; //TP-Message-Reference. The "00" value here lets the phone set the message reference number itself. $ret.="0B"; // Address-Length. Length of phone number (11) $ret.="91"; //Type-of-Address. (91 indicates international format of the phone number). // Начало кодирования номера мобильного if ((strlen($mob)/2)%2){ $mob.="F"; } for ($i=0;$i<strlen($mob);$i+=2){ $ret.=$mob[$i+1].$mob[$i]; } // Закончили взрывать мозг :) $ret.="00"; //TP-PID. Protocol identifier $ret.="18"; //TP-DCS. Data coding scheme. 18 - don't save at history, 08 - save $ret.="C1"; //TP-Validity-Period. C1 means 1 week $ret.=sprintf("%02X",strlen($mess)/2); //TP-User-Data-Length. Length of message. $ret.=$mess; //TP-User-Data $ret.=chr(26); //end of TP-User-Data $len=sprintf ("%s",(strlen($ret)-3)/2); $mess=$ret; print "16-bit encoded SMS<br>"; }else{ print "Длина ссобщения в 16-битной кодировке [$len] больше 70 символов!<br>n"; return -1; } } // Создаем сокет $socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP); if (!$socket) { err($socket); }else{ // Устанавливаем опцию таймаута в 5 секунд для сокета. // Проверка на удачную установку лежит на вас :) socket_set_option($socket,SOL_SOCKET,SO_SNDTIMEO,array("sec"=>5,"usec"=>0)); // Устанавливаем соединение на сокет $result = @socket_connect($socket, $init['ip'], $init['port']); if (!$result) { err($socket); } else { // Инициализируем GSM шлюз $out=sms_init($socket,$len,$debug); if ($len){ // Сообщение на кириллице $out.=raw($socket,"AT+CMGS=$len",">"); }else{ // Сообщение на латинице $out.=raw($socket,sprintf("AT+CMGS="+%s"",$mob),">"); $mess.=chr(26); } $out.=raw($socket,$mess,"CMGS",$debug); if ($debug){ printf ("<pre>[ %s ]</pre>",$out); } } @socket_close($socket); } } return $out;
}
// Функция начальной инициализации модема
// $socket - ресурс сокета
// $type - 0 - сообщение на латинице, 1 - на кириллице
// $debug - 1 - выводить отладочную инфу, 0 - нет
function sms_init($socket,$type,$debug=0){
global $init;
$out=raw($socket,"AT","OK",$debug);
$out.=raw($socket,sprintf("AT+CPIN=%s",$init['pin']),'(ERROR)|(OK)',$debug);
$out.=raw($socket,sprintf("AT+CMGF=%d",$type?0:1),"OK$",$debug);
if ($debug){
print "<pre><font color="green">$out</font></pre><br>";
}
return $out;
}
// Функция посылки комманды на GSM шлюз
// $socket - ресурс сокета
// $write - команда, передаваемая GSM шлюзу
// $delim - последовательность, означающая окончание чтения ответа с GSM шлюза
// $debug - 1 - выводить отладочную инфу, 0 - нет
function raw($socket,$write,$delim,$debug=0){
$ooo='';
$write.="\r\n";
if (@socket_write($socket,$write,strlen($write))===false){
err($socket);
}else{
while ($out = @socket_read($socket, 2, PHP_NORMAL_READ)) {
if ($out===false){
err($socket);
}else{
if ($debug){
print $out;
flush();
}
$ooo=sprintf("%s%s",$ooo,$out);
}
if (preg_match("/".$delim."/i",$ooo,$tmp)){
if ($debug){
print_r($tmp);
flush();
}
break;
}
}
}
usleep(100000);
return $ooo;
}
// Функция вывода ошибки при работе с сокетом
// $soc - ресурс сокета
function err($soc){
printf ("<script>alert ('Ошибка: %s');</script><font color=red><b>%s: [%s]</b></font><br>",
socket_strerror(socket_last_error($soc)),"Ошибка",socket_strerror(socket_last_error($soc)));
}
// Функция кодирования кириллицы из CP1251 в UCS2
// $str - строка для перекодирования
function cp1251_2ucs2($str){
for ($i=0;$i<strlen($str);$i++){
if (ord($str[$i]) < 127){
$results = sprintf("%04X",ord($str[$i]));
}elseif (ord($str[$i])==184){ //ё
$results="0451";
}elseif (ord($str[$i])==168){ //Ё
$results="0401";
}else{
$results = sprintf("%04X",(ord($str[$i])-192+1040));
}
$ucs2 .= $results;
}
return $ucs2;
}
// Функция вывода отладочной инфы
function debug($array){
ob_start("get_vars");
print_r($array);
ob_end_flush();
}
// Функция output_callback для функции debug()
function get_vars($buffer){
return sprintf ("<div style="padding: 3px; border: 2px #88bbbb solid; color: #00dd00; background-color: #bbffbb; font-weight: bold; text-align: left;"><pre>%s</pre></div>",
htmlspecialchars($buffer));
}
?>
Декодирование принятых SMS сообщений на PHP
Отлично, с отсылкой СМС разобрались, осталось чтение сообщений (как обычно, с 8-битной кодировкой проблем нет, есть только с нашей кириллицей).
Команды для чтения SMS с GSM модемов:
function ucs2_2cp1251($str){ for ($i=0;$i<strlen($str);$i+=4){ $char=hexdec($str[$i].$str[$i+1].$str[$i+2].$str[$i+3]); if ($char>126){ if ($char==1105){ $char=184; //ё }elseif($char==1025){ $char=168; //Ё }elseif ($char>=848){ $char-=848; } }elseif(!$char){ $char=32; } $ret.=chr($char); } return $ret; } ?>
Источники: 1. О виджетах и гаджетах 2. SMS and PDU format 3. IXBT.ru 4. Связывание мобильника и FreeBSD через bluetooth (c п.1 по п.6 включительно) Ссылки:
З.Ы. Этот материал можно использовать и при отсылке СМС через обычный мобильник. Разница в том, что вместо сетевого сокета нужно использовать устройство FreeBSD /dev/ttyU0 (при подключении через USB кабель) либо /dev/ttypf (выполнив перед этим команду rfcomm_sppd -a MAC_адрес_телефона -t /dev/ttypf &). О связывании мобильника и FreeBSD через bluetooth смотри выше "Источники". З.З.Ы. При копировании статьи ссылка на источник ОБЯЗАТЕЛЬНА !