В свое время, так как я много времени проводил в командировках, мной была приобретена замечательная игрушка — usb модем Huawei e1550. Но времена лихой молодости прошли, и необходимость в использовании данного девайса по прямому назначению отпала. Так он и пылился у меня на полке в течении нескольких лет. И пылился бы и дальше, но возникла задача сделать систему оповещения. Тут я и вспомнил про модем. Рассмотрев поставленную задачу — был вынужден отказаться от SMS оповещения в пользу голосового дозвона по причине невозможности получить уведомление о прочтении SMS. Решения на базе Asterisk показались мне несколько громоздкими, и почитав доку по модему я решил написать звонилку самостоятельно.
Причина публикации.
Несмотря на обилие статей по работе с USSD и SMS запросами, я не нашел ни одной реализации голосовых вызовов на скриптовых языках (таких как Perl, PHP, Node.js). Надеюсь данная статья будет для Вас хорошим подспорьем.
Среда разработки
операционная система: Linux
Дистрибутив: openSuSe 12.3
Ядро: 3.7.10-1.16-desktop #1 SMP PREEMPT Fri May 31 20:21:23 UTC 2013 (97c14ba) i686 i686 i386 GNU/Linux
Язык программирования: Perl
usb модем: Huawei e1550
Немного теории.
В большинстве дистрибутивов Linux, при подключении данного модема в /dev создаются 3 usb интерфейса. обычно это:
/dev/ttyUSB0 — командный интерфейс модема
/dev/ttyUSB1 — голосовой(при включенном голосовом режиме) интерфейс модема
/dev/ttyUSB2 — командный интерфейс модема. Отличается от /dev/ttyUSB0 тем что с него можно читать не только ответы модема на команды, а также служебные сообщения. Такие как данные о качестве сигнала, вывод ^CEND и прочее.
Для начала работы с модемом достаточно открыть как файл один из командных интерфейсов на чтение и запись.
Чтобы отправить модему команду — нужно записать ее в открытый файл интерфейса.
Чтобы получить ответ модема на данную команду — нужно прочесть его из открытого файла интерфейса.
Команды которые можно подавать модему — это AT команды
Команды для модема Huawei e1550 и ответы которые на них можно получить описаны в его спецификации:
HUAWEI CDMA Datacard Modem AT Command Interface Specification
HUAWEI UMTS Datacard Modem AT Command Interface Specification
Для того чтобы активировать голосовые функции модема нужно подать команду AT^CVOICE=0
Голосовые функции будут активированы до тех пор пока не будут отключены командой AT^CVOICE=1
Для того чтобы начать прием/передачу в модем аудио информации нужно при каждом звонке переключать режим работы аудио порта модема командой AT^DDSETEX=2
Аудиоданные для передачи модему должны иметь формат:
частота оцифровки: 8000 Герц.
количество каналов: 1 (mono).
бит на оцифровку: 16 unsigned.
Аудиоданные должны подаваться в аудио порт модема порциями по 320 байт каждые 0.02 секунды.
По завершении вызова модем через 2-й командный интерфейс выдает информацию о вызове в виде сообщения CEND
формат вывода ^CEND:call_index, duration, end_status, cc_cause
где:
call_index — уникальный идентификатор вызова
duration — длительность вызова в секундах
end_status — код статуса устройства после завершения вызова
cc_cause — код причины завершения вызова
Итак. Начнем.
звонилка будет состоять из 3-х файлов:
huawey_voice_call.pl — непосредственно сам скрипт голосового дозвона.
list.01.pl — файл с данными абонентов.
test.voice.raw — файл с голосовым сообщением записанным в нужном формате.
также в конце статьи будут представлены 2 дополнительных файла:
cc_cause.pl — содержит коды причин завершения вызова (cc_cause)
end_status.pl — содержит коды статуса устройства после завершения вызова (end_status)
все файлы одним архивом (выложил на своем компе, изредка комп бывает выключен)
рассмотрим huawey_voice_call.pl
- #!/usr/bin/perl
- # подключаем модуль Time::HiRes и импортируем
- # в текущее пространство имен функцию sleep
- # особенность данной функции - возможность указывать
- # задержку меньше секунды
- use Time::HiRes qw(sleep);
- # подключаем файл cc_cause.pl содержащий коды Disconnect cause codes
- # Данные коды отображают причину завершения вызова
- # В данном скрипте не используются, но для написания
- # полноценной звонилки необходимо анализировать данный
- # параметр в выводе ^CEND:call_index, duration, end_status, cc_cause
- # my %cc_cause = do 'cc_cause.pl';
- # подключаем файл end_status.pl содержащий коды Call endind cause codes
- # Данные коды отображают статус устройства после завершения вызова
- # В данном скрипте не используются, но для написания
- # полноценной звонилки необходимо анализировать данный
- # параметр в выводе ^CEND:call_index, duration, end_status, cc_cause
- # my %end_status = do 'end_status.pl';
- # Для информации:
- # Сообщения типа CEND выдаются модемом при завершении вызова
- # и содержат в себе информацию о вызове, о причине завершения вызова
- # и о состоянии устройства.
- # формат вывода ^CEND:call_index, duration, end_status, cc_cause
- # где:
- # call_index - уникальный идентификатор вызова
- # duration - длительность вызова в секундах
- # end_status - код статуса устройства после завершения вызова
- # cc_cause - код причины завершения вызова
- # при подключении модема к компьютеру с OS Linux
- # создаются 3 usb интерфейса для обмена данными с модемом
- # обычно это:
- # /dev/ttyUSB0 - командный интерфейс модема
- # /dev/ttyUSB1 - голосовой(при включенном голосовом режиме) интерфейс модема
- # /dev/ttyUSB2 - командный интерфейс модема. Отличается от /dev/ttyUSB0 тем
- # что с него можно читать не только ответы модема на команды, а также служебные
- # сообщения. Такие как данные о качестве сигнала, вывод ^CEND и прочее
- # указываем порт для отсылки модему звука
- $VOICE_PORT = "/dev/ttyUSB1";
- # указываем порт для подачи модему команд
- $COMMAND_PORT = "/dev/ttyUSB2";
- # устанавливаем в:
- # 0 - чтобы отключить вывод отладочной информации
- # 1 - чтобы включить вывод отладочной информации
- $VERBOSE = 1;
- # Открываем командный порт модема на чтение и запись
- open my $SENDPORT, '+<', $COMMAND_PORT or die "Can't open '$COMMAND_PORT': $!n";
- # Открываем голосовой порт модема на чтение и запись
- # чтение аудиопотока из порта в данной программе не используется
- # но вам ничто не мешает превратить данный скрипт в автоответчик например
- open my $SENDPORT_WAV, '+<', $VOICE_PORT or die "Can't open '$VOICE_PORT': $!n";
- # подключаем файл list.01.pl содержащий данные об абонентах
- my @user_list = do 'list.01.pl';
- # вызываем функцию обзвона, которой передаются 2 параметра:
- # 1-й - имя файла с голосовым сообщением
- # 2-й - массив с данными об абонентах
- call_list("test.voice.raw", @user_list );
- # по окончании обзвона закрываем все открытые файлы/порты
- exit_call();
- # данная функция производит обзвон абонентов по списку
- sub call_list{
- # получаем имя файла с голосовым сообщением
- my $l_file = shift;
- # получаем ссылку на список с данными об абонентах
- my $l_list = shift;
- # загружаем данные из файла с голосовым сообщением
- my $l_voice = load_voice($l_file);
- # данный цикл пробегает по списку абонентов
- # и пытается произвести дозвон
- foreach $l_info (@{$l_list}){
- # вызываем функцию дозвона до абонента
- my $l_msg = call_one($l_info,$l_voice);
- # Выводим полученное сообщение
- print $l_msg;
- # прежде чем звонить следующему абоненту
- # ждем 3 секунды.
- sleep 3;
- }
- }
- # данная функция производит попытку вызова указаного номера
- # и в случае успеха - транслирует голосовое сообщение
- sub call_one{
- my $l_info = shift; # ХЭШ с данными текущего абонента
- my $l_bufer = shift; # массив с 320 байтными кусками головового сообщения
- # данная команда включает в модеме голосовой режим
- # один раз включив его можно удалить/заремарить
- # эту команду. Модем запомнит состояние.
- #at_send('AT^CVOICE=0');
- # отдаем модему команду дозвониться до номера $l_info->{phone}
- # и ожидать ответа от модема:
- # OK - дозвон прошел успешно
- # NO CARRIER - нет соединения с сотовой сетью
- my $l_rec = at_send("ATD$l_info->{phone};",qr/(OK|NO CARRIER)/);
- # в случае если дозвон не произошел - выходим из функции и возвращаем соответствующее сообщение
- return "Абонент $l_info->{name} [$l_info->{phone}] не оповещен. НЕТ СЕТИn" if $l_rec eq 'NO CARRIER';
- # ожидаем когда абонент поднимет трубку
- # CONN:.... - абонент поднял трубку
- # CEND:.... - абонент недоступен, занят или сбросил вызов
- $l_rec = at_rec(qr/^(CONN:1,0|CEND:)/);
- # в случае если абонент не поднял трубку - выходим из функции и возвращаем соответствующее сообщение
- return "Абонент $l_info->{name} [$l_info->{phone}] не оповещен. НЕДОСТУПЕН или СБРОСИЛn" if $l_rec eq 'CEND:';
- # переключаем модем в режим приема/передачи голоса
- # OK - переключение прошло успешно
- # ERROR - переключение не произведено
- # CEND:.... - абонент недоступен, занят или сбросил вызов
- $l_rec = at_send('AT^DDSETEX=2',qr/(OK|ERROR|CEND:)/);
- # в случае если не удалось переключится в голосовой режим или абонент не поднял
- # трубку - выходим из функции и возвращаем соответствующее сообщение
- return "Абонент $l_info->{name} [$l_info->{phone}] не оповещен. НЕДОСТУПЕН или СБРОСИЛn" if $l_rec ne 'OK';
- # Если дошли до сюда - значит вызов установлен и абонент поднял трубку
- # Звук модему должен передаваться порциями по 320 байт каждые 0.02 секунды
- # Устанавливаем служебную переменную $| в единицу это отключает буферизацию.
- # Таким образом данные в звуковой порт будут отправляться незамедлительно.
- $|=1;
- # Цикл по буферу с 320 байтными кусками голосового сообщения
- foreach my $c (@{$l_bufer}) {
- # Запись очередного куска в голосовой порт модема
- syswrite $SENDPORT_WAV, $c, 320;
- # Ожидаем 0.02 секунды перед тем как продолжить цикл
- sleep(0.02);
- }
- # Вешаем трубку.
- at_send('AT+CHUP');
- # Возвращаем сообщение об успешном оповещении.
- return "Абонент $l_info->{name} [$l_info->{phone}] УСПЕШНО ОПОВЕЩЕНn";
- }
- # данная функция загружает голосовое сообщение в массив кусками по 320 байт
- # принимает 1 параметр - имя файла
- # формат звуковых данных - pcm, моно, 8000 кГц, 16 бит, unsigned
- sub load_voice{
- my $l_file_name = shift;
- my $l_fh = new IO::File "< $l_file_name" or die "Cannot open $l_file_name : $!";
- binmode($l_fh);
- my @l_bufer = ();
- while (read($l_fh,$l_bufer[$i],320)) { $i++; }
- close $l_fh;
- return @l_bufer;
- }
- # данная функция отправляет команду в командный порт модема
- # и ждет ответа указанного в регулярном выражении
- # принимает 2 параметра:
- # 1-й - команда
- # 2-й - регулярное выражение описывающее варианты ожидаемых ответов (по умолчанию OK)
- sub at_send{
- my $l_cmd = shift;
- my $l_rx = shift || qr/(OK)/;
- print $SENDPORT "$l_cmdr";
- print "SEND: [$l_cmd]n" if $VERBOSE;
- return at_rec($l_rx);
- }
- # данная функция ждет от модема ответа указанного в регулярном выражении
- # принимает 1 параметр - регулярное выражение описывающее варианты ожидаемых ответов (по умолчанию OK)
- sub at_rec{
- my $l_rx = shift || qr/OK/;
- my $recive='';
- while ( !($recive=~$l_rx) ) {
- $recive=<$SENDPORT>;
- $recive=~s/[nr]+//msg;
- print "RECIVE: [$recive]n" if $VERBOSE && $recive;
- }
- $recive=~$l_rx;
- print "END RECIVE: [$recive] [$1] [$l_rx]n" if $VERBOSE;
- return $1;
- }
- # данная функция закрывает ранее открытые порты модема
- sub exit_call{
- print "ОПОВЕЩЕНИЕ ОКОНЧЕНОn";
- close $SENDPORT_WAV;
- at_send('AT+CHUP');
- close $SENDPORT;
- }
рассмотрим list.01.pl
# Список абонентов.
# Это массив хэш массивов в котором каждая запись содержит
# данные о абоненте:
# phone - телефон абонента
# name - ФИО абонента
# Также возможно хранение и других данных об абоненте
(
{ phone => '+79111234567', name => 'Петров Петр Петрович' },
{ phone => '+79117654321', name => 'Васильев Василий Васильевич' }
);
рассмотрим test.voice.raw
Для создания данного файла использовался аудиоредактор Audacity как показано на картинках:
Также привожу дополнительные файлы cc_cause.pl и end_status.pl. Они не используются в представленной версии скрипта, но в случае доработки будут полезны.
cc_cause.pl
# коды disconnect cause (cc)
# English http://www.eversoft.net/dcc.html
# по Русски http://ru.wikipedia.org/wiki/Q.931
# маны по huawei
# HUAWEICDMADatacard ModemAT Command Interface Specification
# "http://www.letswireless.com.cn/asp_bin/downfile/2009929121443234.pdf"
#
# HUAWEICDMADatacard ModemAT Command Interface Specification
# "http://www.net139.com/UploadFile/menu/HUAWEI%20UMTS%20Datacard%20Modem%20AT%20Command%20Interface%20Specification_V2.3.pdf"
(
'1' => 'UNASSIGNED_CAUSE',
'3' => 'NO_ROUTE_TO_DEST',
'6' => 'CHANNEL_UNACCEPTABLE',
'8' => 'OPERATOR_DETERMINED_BARRING',
'16' => 'NORMAL_CALL_CLEARING',
'17' => 'USER_BUSY',
'18' => 'NO_USER_RESPONDING',
'19' => 'USER_ALERTING_NO_ANSWER',
'21' => 'CALL_REJECTED',
'22' => 'NUMBER_CHANGED',
'26' => 'NON_SELECTED_USER_CLEARING',
'27' => 'DESTINATION_OUT_OF_ORDER',
'28' => 'INVALID_NUMBER_FORMAT',
'29' => 'FACILITY_REJECTED',
'30' => 'RESPONSE_TO_STATUS_ENQUIRY',
'31' => 'NORMAL_UNSPECIFIED',
'34' => 'NO_CIRCUIT_CHANNEL_AVAILABLE',
'38' => 'NETWORK_OUT_OF_ORDER',
'41' => 'TEMPORARY_FAILURE',
'42' => 'SWITCHING_EQUIPMENT_CONGESTION',
'43' => 'ACCESS_INFORMATION_DISCARDED',
'44' => 'REQUESTED_CIRCUIT_CHANNEL_NOT_AVAILABLE',
'47' => 'RESOURCES_UNAVAILABLE_UNSPECIFIED',
'49' => 'QUALITY_OF_SERVICE_UNAVAILABLE',
'50' => 'REQUESTED_FACILITY_NOT_SUBSCRIBED',
'55' => 'INCOMING_CALL_BARRED_WITHIN_CUG',
'57' => 'BEARER_CAPABILITY_NOT_AUTHORISED',
'58' => 'BEARER_CAPABILITY_NOT_PRESENTLY_AVAILABLE',
'63' => 'SERVICE_OR_OPTION_NOT_AVAILABLE',
'65' => 'BEARER_SERVICE_NOT_IMPLEMENTED',
'68' => 'ACM_GEQ_ACMMAX',
'69' => 'REQUESTED_FACILITY_NOT_IMPLEMENTED',
'70' => 'ONLY_RESTRICTED_DIGITAL_INFO_BC_AVAILABLE',
'79' => 'SERVICE_OR_OPTION_NOT_IMPLEMENTED',
'81' => 'INVALID_TRANSACTION_ID_VALUE',
'87' => 'USER_NOT_MEMBER_OF_CUG',
'88' => 'INCOMPATIBLE_DESTINATION',
'91' => 'INVALID_TRANSIT_NETWORK_SELECTION',
'95' => 'SEMANTICALLY_INCORRECT_MESSAGE',
'96' => 'INVALID_MANDATORY_INFORMATION',
'97' => 'MESSAGE_TYPE_NON_EXISTENT',
'98' => 'MESSAGE_TYPE_NOT_COMPATIBLE_WITH_PROT_STATE',
'99' => 'IE_NON_EXISTENT_OR_NOT_IMPLEMENTED',
'100' => 'CONDITIONAL_IE_ERROR',
'101' => 'MESSAGE_NOT_COMPATIBLE_WITH_PROTOCOL_STATE',
'102' => 'RECOVERY_ON_TIMER_EXPIRY',
'111' => 'PROTOCOL_ERROR_UNSPECIFIED',
'127' => 'INTERWORKING_UNSPECIFIED',
'160' => 'REJ_UNSPECIFIED',
'161' => 'AS_REJ_RR_REL_IND',
'162' => 'AS_REJ_RR_RANDOM_ACCESS_FAILURE',
'163' => 'AS_REJ_RRC_REL_IND',
'164' => 'AS_REJ_RRC_CLOSE_SESSION_IND',
'165' => 'AS_REJ_RRC_OPEN_SESSION_FAILURE',
'166' => 'AS_REJ_LOW_LEVEL_FAIL',
'167' => 'AS_REJ_LOW_LEVEL_FAIL_REDIAL_NOT_ALLOWD',
'168' => 'MM_REJ_INVALID_SIM',
'169' => 'MM_REJ_NO_SERVICE',
'170' => 'MM_REJ_TIMER_T3230_EXP',
'171' => 'MM_REJ_NO_CELL_AVAILABLE',
'172' => 'MM_REJ_WRONG_STATE',
'173' => 'MM_REJ_ACCESS_CLASS_BLOCKED',
'174' => 'ABORT_MSG_RECEIVED',
'175' => 'OTHER_CAUSE',
'176' => 'CNM_REJ_TIMER_T303_EXP',
'177' => 'CNM_REJ_NO_RESOURCES',
'178' => 'CNM_MM_REL_PENDING',
'179' => 'CNM_INVALID_USER_DATA'
);
end_status.pl
# коды Call ending cause codes
# маны по huawei
#
# HUAWEICDMADatacard ModemAT Command Interface Specification
# "http://www.letswireless.com.cn/asp_bin/downfile/2009929121443234.pdf"
#
# HUAWEICDMADatacard ModemAT Command Interface Specification
# "http://www.net139.com/UploadFile/menu/HUAWEI%20UMTS%20Datacard%20Modem%20AT%20Command%20Interface%20Specification_V2.3.pdf"
(
'0' => 'The board is offline.',
'21' => 'Board is out of service.',
'22' => 'Call is ended normally.',
'23' => 'Call is interrupted by BS.',
'24' => 'BS record is received during a call.',
'25' => 'BS releases a call.',
'26' => 'BS rejects the current SO service.',
'27' => 'There is incoming BS call.',
'28' => 'received alert stop from BS.',
'29' => 'Call is ended normally by the client end.',
'30' => 'received end activation — OTASP call.',
'31' => 'MC ends call initiation or call.',
'34' => 'RUIM is not available.',
'99' => 'NDSS error.',
'100' => 'rxd a reason from lower layer,look in cc_cause',
'101' => 'After a MS initiates a call, the network fails to respond.',
'102' => 'MS rejects an incoming call.',
'103' => 'A call is rejected during the put-through process.',
'104' => 'The release is from the For details, check',
'105' => 'The phone fee is used up.',
'106' => 'The MS is out of the service'
);
В завершение.
Данная версия скрипта голосового оповещения не претендует на полноту и правильность реализации, а является лишь демонстрацией, и для серьезного использования может быть и должна быть усовершенствована. Необходимо добавить более серьезную обработку состояний CEND, реализовать условия повторного дозвона до абонентов, если с первого раза не удалось оповестить. Также можно сделать web интерфейс включающий в себя планировщик задач, редактор списков абонентов, генерацию отчетов и многое другое.
Я надеюсь что эта статья окажется востребованной и полезной для Вас, а также постараюсь и впредь выкладывать интересные и полезные статьи.
Автор: lastuniverse