perl скрипт производящий голосовой обзвон (оповещение) через usb модем huawei e1550

в 22:20, , рубрики: Без рубрики

В свое время, так как я много времени проводил в командировках, мной была приобретена замечательная игрушка — 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

  1. #!/usr/bin/perl
  2.  
  3. # подключаем модуль Time::HiRes и импортируем
  4. # в текущее пространство имен функцию sleep
  5. # особенность данной функции - возможность указывать
  6. # задержку меньше секунды
  7. use Time::HiRes qw(sleep);
  8.  
  9.  
  10.  
  11. # подключаем файл cc_cause.pl содержащий коды Disconnect cause codes
  12. # Данные коды отображают причину завершения вызова
  13. # В данном скрипте не используются, но для написания
  14. # полноценной звонилки необходимо анализировать данный
  15. # параметр в выводе ^CEND:call_index, duration, end_status, cc_cause
  16. # my %cc_cause = do 'cc_cause.pl'; 
  17.  
  18. # подключаем файл end_status.pl содержащий коды Call endind cause codes
  19. # Данные коды отображают статус устройства после завершения вызова
  20. # В данном скрипте не используются, но для написания
  21. # полноценной звонилки необходимо анализировать данный
  22. # параметр в выводе ^CEND:call_index, duration, end_status, cc_cause
  23. # my %end_status = do 'end_status.pl';
  24.  
  25. # Для информации:
  26. # Сообщения типа CEND выдаются модемом при завершении вызова
  27. # и содержат в себе информацию о вызове, о причине завершения вызова
  28. # и о состоянии устройства.
  29. # формат вывода ^CEND:call_index, duration, end_status, cc_cause
  30. # где:
  31. # call_index - уникальный идентификатор вызова
  32. # duration - длительность вызова в секундах
  33. # end_status - код статуса устройства после завершения вызова
  34. # cc_cause - код причины завершения вызова
  35.  
  36. # при подключении модема к компьютеру с OS Linux
  37. # создаются 3 usb интерфейса для обмена данными с модемом
  38. # обычно это:
  39. # /dev/ttyUSB0 - командный интерфейс модема
  40. # /dev/ttyUSB1 - голосовой(при включенном голосовом режиме) интерфейс модема
  41. # /dev/ttyUSB2 - командный интерфейс модема. Отличается от /dev/ttyUSB0 тем
  42. # что с него можно читать не только ответы модема на команды, а также служебные
  43. # сообщения. Такие как данные о качестве сигнала, вывод ^CEND и прочее
  44.  
  45. # указываем порт для отсылки модему звука
  46. $VOICE_PORT = "/dev/ttyUSB1";
  47.  
  48. # указываем порт для подачи модему команд
  49. $COMMAND_PORT = "/dev/ttyUSB2";
  50.  
  51. # устанавливаем в:
  52. # 0 - чтобы отключить вывод отладочной информации
  53. # 1 - чтобы включить вывод отладочной информации
  54. $VERBOSE = 1;
  55.  
  56. # Открываем командный порт модема на чтение и запись
  57. open my $SENDPORT, '+<', $COMMAND_PORT or die "Can't open '$COMMAND_PORT': $!n";
  58.  
  59. # Открываем голосовой  порт модема на чтение и запись
  60. # чтение аудиопотока из порта в данной программе не используется
  61. # но вам ничто не мешает превратить данный скрипт в автоответчик например
  62. open my $SENDPORT_WAV, '+<', $VOICE_PORT or die "Can't open '$VOICE_PORT': $!n";
  63.  
  64.  
  65.  
  66. # подключаем файл list.01.pl содержащий данные об абонентах
  67. my @user_list = do 'list.01.pl'; 
  68.  
  69.  
  70. # вызываем функцию обзвона, которой передаются 2 параметра:
  71. # 1-й - имя файла с голосовым сообщением
  72. # 2-й - массив с данными об абонентах
  73. call_list("test.voice.raw", @user_list );
  74.  
  75. # по окончании обзвона закрываем все открытые файлы/порты
  76. exit_call();
  77.  
  78.  
  79.  
  80. # данная функция производит обзвон абонентов по списку
  81. sub call_list{
  82.     # получаем имя файла с голосовым сообщением
  83.     my $l_file = shift;
  84.  
  85.     # получаем ссылку на список с данными об абонентах
  86.     my $l_list = shift;
  87.  
  88.     # загружаем данные из файла с голосовым сообщением
  89.     my $l_voice = load_voice($l_file);
  90.  
  91.     # данный цикл пробегает по списку абонентов
  92.     # и пытается произвести дозвон
  93.     foreach $l_info (@{$l_list}){
  94.  
  95. # вызываем функцию дозвона до абонента
  96. my $l_msg = call_one($l_info,$l_voice);
  97.  
  98. # Выводим полученное сообщение
  99. print $l_msg;
  100.  
  101. # прежде чем звонить следующему абоненту
  102. # ждем 3 секунды.
  103. sleep 3;
  104.     }
  105. }
  106.  
  107.  
  108. # данная функция производит попытку вызова указаного номера
  109. # и в случае успеха - транслирует голосовое сообщение
  110. sub call_one{
  111.  
  112.     my $l_info = shift;  # ХЭШ с данными текущего абонента
  113.     my $l_bufer = shift; # массив с 320 байтными кусками головового сообщения
  114.  
  115.     # данная команда включает в модеме голосовой режим
  116.     # один раз включив его можно удалить/заремарить
  117.     # эту команду. Модем запомнит состояние.
  118.     #at_send('AT^CVOICE=0'); 
  119.  
  120.     # отдаем модему команду дозвониться до номера $l_info->{phone}
  121.     # и ожидать ответа от модема:
  122.     # OK - дозвон прошел успешно
  123.     # NO CARRIER - нет соединения с сотовой сетью
  124.     my $l_rec = at_send("ATD$l_info->{phone};",qr/(OK|NO CARRIER)/);
  125.     # в случае если дозвон не произошел - выходим из функции и возвращаем соответствующее сообщение
  126.     return "Абонент $l_info->{name} [$l_info->{phone}] не оповещен. НЕТ СЕТИn"  if  $l_rec eq 'NO CARRIER';
  127.  
  128.     # ожидаем когда абонент поднимет трубку
  129.     # CONN:.... - абонент поднял трубку
  130.     # CEND:.... - абонент недоступен, занят или сбросил вызов
  131.     $l_rec = at_rec(qr/^(CONN:1,0|CEND:)/);
  132.     # в случае если абонент не поднял трубку - выходим из функции и возвращаем соответствующее сообщение
  133.     return "Абонент $l_info->{name} [$l_info->{phone}] не оповещен. НЕДОСТУПЕН или СБРОСИЛnif  $l_rec eq 'CEND:';
  134.  
  135.     # переключаем модем в режим приема/передачи голоса
  136.     # OK - переключение прошло успешно
  137.     # ERROR - переключение не произведено
  138.     # CEND:.... - абонент недоступен, занят или сбросил вызов
  139.     $l_rec = at_send('AT^DDSETEX=2',qr/(OK|ERROR|CEND:)/);
  140.     # в случае если не удалось переключится в голосовой режим или абонент не поднял
  141.     # трубку - выходим из функции и возвращаем соответствующее сообщение
  142.     return "Абонент $l_info->{name} [$l_info->{phone}] не оповещен. НЕДОСТУПЕН или СБРОСИЛnif  $l_rec ne 'OK';
  143.  
  144.     # Если дошли до сюда - значит вызов установлен и абонент поднял трубку
  145.     # Звук модему должен передаваться порциями по 320 байт каждые 0.02 секунды
  146.  
  147.     # Устанавливаем служебную переменную $| в единицу это отключает буферизацию.
  148.     # Таким образом данные в звуковой порт будут отправляться незамедлительно.
  149.     $|=1;
  150.  
  151.     # Цикл по буферу с 320 байтными кусками голосового сообщения
  152.     foreach my $c (@{$l_bufer}) {
  153.     # Запись очередного куска в голосовой порт модема
  154.     syswrite  $SENDPORT_WAV, $c, 320;
  155.  
  156.     # Ожидаем 0.02 секунды перед тем как продолжить цикл
  157.     sleep(0.02);
  158.     }
  159.  
  160.     # Вешаем трубку.
  161.     at_send('AT+CHUP');
  162.  
  163.     # Возвращаем сообщение об успешном оповещении.
  164.     return "Абонент $l_info->{name} [$l_info->{phone}] УСПЕШНО ОПОВЕЩЕНn";
  165. }
  166.  
  167. # данная функция загружает голосовое сообщение в массив кусками по 320 байт
  168. # принимает 1 параметр - имя файла
  169. # формат звуковых данных - pcm, моно, 8000 кГц, 16 бит, unsigned
  170. sub load_voice{
  171.     my $l_file_name = shift;
  172.     my $l_fh = new IO::File "< $l_file_name" or die "Cannot open $l_file_name : $!";
  173.     binmode($l_fh);
  174.     my @l_bufer = ();
  175.     while (read($l_fh,$l_bufer[$i],320)) { $i++; }
  176.     close $l_fh;
  177.     return @l_bufer;
  178. }
  179.  
  180.  
  181. # данная функция отправляет команду в командный порт модема
  182. # и ждет ответа указанного в регулярном выражении
  183. # принимает 2 параметра:
  184. # 1-й - команда
  185. # 2-й - регулярное выражение описывающее варианты ожидаемых ответов (по умолчанию OK)
  186. sub at_send{
  187.     my $l_cmd = shift;
  188.     my $l_rx = shift || qr/(OK)/;
  189.     print $SENDPORT "$l_cmdr";
  190.     print "SEND: [$l_cmd]nif $VERBOSE;
  191.     return at_rec($l_rx);
  192. }
  193.  
  194.  
  195. # данная функция ждет от модема ответа указанного в регулярном выражении 
  196. # принимает 1 параметр - регулярное выражение описывающее варианты ожидаемых ответов (по умолчанию OK)
  197. sub at_rec{
  198.     my $l_rx = shift || qr/OK/;
  199.     my $recive='';
  200.     while ( !($recive=~$l_rx) ) {
  201. $recive=<$SENDPORT>;
  202. $recive=~s/[nr]+//msg;
  203. print "RECIVE: [$recive]nif $VERBOSE && $recive;
  204.     }
  205.     $recive=~$l_rx;
  206.     print "END RECIVE: [$recive] [$1] [$l_rx]nif $VERBOSE;
  207.     return $1;
  208. }
  209.  
  210.  
  211. # данная функция закрывает ранее открытые порты модема
  212. sub exit_call{
  213.     print "ОПОВЕЩЕНИЕ ОКОНЧЕНОn";
  214.     close $SENDPORT_WAV;
  215.     at_send('AT+CHUP');
  216.     close $SENDPORT;
  217. }
  218.  

рассмотрим list.01.pl

# Список абонентов.
# Это массив хэш массивов в котором каждая запись содержит
# данные о абоненте:
# phone - телефон абонента
# name  - ФИО абонента
# Также возможно хранение и других данных об абоненте
(
    { phone => '+79111234567', name => 'Петров Петр Петрович' },
    { phone => '+79117654321', name => 'Васильев Василий Васильевич' }
);

рассмотрим test.voice.raw
Для создания данного файла использовался аудиоредактор Audacity как показано на картинках:

image

image

image

image

image

Также привожу дополнительные файлы 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js