ДЕНИС СОКОЛОВ
Создаём систему учета исходящих телефонных звонков
Сегодня мы займемся созданием системы учета исходящих телефонных звонков на примере УАТС LG GDK-162. От вас требуется: навыки работы в UNIX-подобных операционных системах, умение программировать на Perl и базовые знания в SQL.
Недавно руководство поставило задачу – необходимо вести учет всех исходящих телефонных звонков. В офисе установлена УАТС LG GDK-162 емкостью 48 внутренних номеров и 8 внешних линий. Различные программы тарификации имеются в избытке. Но ни одна меня не устроила. Большинство из них платные и написаны под Windows, из некоммерческих только SMDR 1.0 поддерживает LG GDK-162.
Для операционных систем Linux и FreeBSD существует очень интересный проект ATSlog: http://www.atslog.dp.ua. Вот описание с сайта программы: «ATSlog предоставляет удобный интерфейс с доступом через веб-браузер для просмотра и анализа звонков различных моделей мини-АТС. Программа бесплатная, распространяется под лицензией GPL, имеет полностью открытый код. Программа успешно работает с моделями Panasonic KX-TA308, KX-TA308RU, KX-TA616RU, KX-TD816RU, KX-TD1232; Samsung SKP-816». К сожалению, не нашел в списке поддерживаемых АТС LG GDK. Для добавления поддержки нужной модели можно отослать автору образцы текстовых лог-файлов. Но это требует времени. Я решил попробовать справиться с задачей своими силами. Это оказалось несложным. Надеюсь, что мой опыт окажется вам полезен.
Рисунок 1. Схема учета исходящих телефонных звонков
LG GDK-162, как и большинство офисных АТС, можно подключить к компьютеру через порт RS-232. Параметры порта: 9600 бит/с, 8 бит данных, без контроля паритета, 1 стоповый бит.
Таблица 1. Распайка кабеля (стандартный нуль-модем)
PBX (9 pin)
|
PC (9 pin)
|
PC (25 pin)
|
2 (TX)
|
2 (RX)
|
3 (RX)
|
3 (RX)
|
3 (TX)
|
2 (TX)
|
5 (GND)
|
5 (GND)
|
7 (GND)
|
Я подключился к последовательному порту УАТС при помощи терминальной программы и стал анализировать считываемые данные. Оказалось, что LG GDK-162 протоколирует свою работу в режиме реального времени, а записи об исходящих звонках выглядят следующим образом:
1035 144 06 00:11 21/08/2005 16:21 O1234567 ** |
Рассмотрим её содержимое:
- первое поле – порядковый номер записи;
- второе – номер станции, с которой сделан вызов;
- третье – номер внешней линии;
- четвертое – длительность звонка (mm:ss);
- пятое – дата;
- шестое – время (hh:mm);
- седьмое – вызванный номер с ведущим символом O. Седьмое поле может кроме цифр содержать символы «#» и «*» – это происходит при звонках на голосовые шлюзы операторов IP-телефонии.
Таким образом, задача учета исходящих телефонных звонков свелась к написанию программы для считывания журнала работы УАТС через порт RS-232, обработки и сохранения соответствующих записей. Можно приступать к реализации.
Исходные данные: учет будет осуществляться на машине Cel 1200/RAM 128 Мб/HDD 20 Гб, операционная система – Debian GNU/Linux 3.1r Sarg, СУБД – PostgreSQL v. 7.4.7, УАТС подключена к последовательному порту /dev/ttyS1.
Создадим пользователя, от которого будет работать программа и рабочий каталог. Кроме этого, изменим права доступа к /dev/ttyS1:
# sudo useradd -d /var/gdklog -s /bin/sh gdk
# sudo passwd gdk
# sudo mkdir /var/gdklog
# sudo chown gdk:gdk /var/gdklog
# sudo chown root:gdk /dev/ttyS1
# sudo chmod 640 /dev/ttyS1
Создадим пользователя и базу данных в PostgreSQL:
# createuser -U postgres -A -D gdk
# createdb -U postgres -O gdk pbxbilling
Создадим таблицу gdklog для хранения статистики. Таблица имеет пять полей:
- d_time – дата и время;
- station – станция;
- line – внешняя линия;
- t_call – продолжительность разговора;
- c_number – вызванный номер (я выбрал для этого поля тип numeric, т.е. номер телефона сохраняется, как число, при этом ведущие нули усекаются, например, номер 01 сохранится в БД как 1).
# vi gdklog.sql
CREATE TABLE gdklog (
"d_time" timestamp,
"station" int2,
"line" int2,
"t_call" time,
"c_number" numeric (30, 0)
);
REVOKE ALL on "gdklog" from PUBLIC;
GRANT ALL on "gdklog" to "gdk";
# psql -U gdk -d pbxbilling < gdklog.sql
Писать программу учета я решил на perl. Полный текст вы можете скачать с сайта www.samag.ru, раздел «Исходный код». Для чтения данных из последовательного порта вполне подойдут стандартные функции для работы с файлами. Естественно, порт необходимо предварительно настроить. Для этого я использовал вызов программы sty:
my $ttys = "/dev/ttyS1";
# Количество строк журнала, кэшируемых в памяти
my $m_cache = 50;
system ("/bin/stty -F $ttys 9600 cs8 -parenb -cstopb");
# Читаем данные из последовательного порта
open (TTYS, "< $ttys") or die "Can’t open $ttys!";
while (<TTYS>)
{
parse_log;
}
Функция parse_log – единственная в программе, специфичная для LG GDK-162.
sub parse_log
{
# Если массив log содержит m_cache строк, то заносим данные в БД
if (@log >= $m_cache)
{
push_to_db;
}
# Отбираем строки фиксирующие исходящие звонки
if (/O\d+/)
{
# Удаляем символы *, #, O
s/[\*,O,\#]+//g;
# Сохраняем полученную строку в массив log
push (@log, $_);
}
print;
}
Допустим, надо добавить поддержку АТС, пишущую протокол в формате:
2005-08-21 16-21-00 06 1234567 00:00:11 144 |
Для этого достаточно будет дописать еще одну конструкцию if в функцию parse_log (кроме этого, надо не забыть проверить параметры последовательного порта).
if (/^(d{4}-d{2}-d{2})s+(d{2}-d{2}-d{2})s+(d{2})s+(d+)s+(d{2}:d{2}:d{2})s+
{
push (@log, "00 $6 $3 $5 $1 $2 $4");
}
Постоянно держать открытым соединение с базой данных – не очень хорошая идея. Поэтому все строки, соответствующие регулярному выражению «/Od/g» (журнал исходящих вызовов), сначала заносятся в массив @log. Как только в нем накапливается $m_cache строк – вызывается процедура push_to_db, которая устанавливает соединение с базой pbxbilling и записывает данные в таблицу gdklog:
# $d_time // Дата и время
# $station // Внутренний номер
# $line // Внешняя линия
# $t_call // Продолжительность вызова
# $date // Дата
# $time // Время
# $c_number // Вызванный номер
my ($d_time, $station, $line, $t_call, $date,
$time, $c_number, @log);
# Параметры соединения с базой данных
my $base = "pbxbilling";
my $user = "gdk";
my $pass = "";
sub push_to_db
{
# Подготовка соединения
my $dbh=DBI->connect("DBI:Pg:dbname=$base",
"$user", "$pass",
{PrintError => 0, RaiseError => 0}
) or return 2;
# Подготовка запроса
my $ins = $dbh->prepare(q{
INSERT INTO gdklog (d_time , station, line, t_call, c_number)
VALUES (?, ?, ?, ?, ?)
});
# Перебираем в цикле все сохраненные строки
foreach my $log (@log)
{
# Разбираем строку
(undef, $station, $line, $t_call, $date, $time, $c_number) = split (/\s+/, $log);
if (length ($t_call) < 6)
{
$t_call="00:$t_call";
}
$d_time = "$date $time";
# Выполняем INSERT
$ins->execute($d_time, $station, $line, $t_call, $c_number)
or return 2;
}
# Очистим массив
undef (@log);
$dbh->disconnect();
return 0;
}
Кроме этого, мне нужно было, чтобы программа могла работать в режиме демона:
# В режиме демона в этот файл перенаправляем все сообщения об ошибках
my $err_file = "/var/gdklog/gdklogd.err";
# PID-файл
my $pid_file = "/var/gdklog/gdklogd.pid";
sub begin_daemon
{
# Делаем fork
my $pid = fork;
exit if $pid;
die "Couldn’t fork: $!" unless defined($pid);
# Сохраняем PID в файл
open (F_PID, ">$pid_file") or die "Can’t open $pid_file: $!";
print F_PID "$$\n";
close F_PID;
# Перенаправляем вывод STDERR в файл
open (*STDERR, ">> $err_file") or die "Can’t reopen *STDERR to $err_file: $!";
# Перенаправляем STDIN и STDOUT в /dev/null
for my $handle (*STDIN, *STDOUT)
{
open ($handle, "> /dev/null") or die "Can’t reopen $handle to /dev/null: $!";
}
# Установка sid процесса
POSIX::setsid()
or die "Can’t start a new session: $!";
}
Чтобы обеспечить целостность данных, я установил обработчики сигналов INT и TERM. Получая один из них, программа будет пытаться немедленно записать данные в базу, если это окончится неудачей (например, сервер БД отключен), то выполнится процедура dump_to_file, которая просто запишет содержимое @log в текстовый файл. После этого работа программы будет завершена. Второй обработчик добавляет возможность записи данных в базу по сигналу HUP без выхода из программы.
$SIG{INT} = $SIG{TERM} = sub { dump_to_file if push_to_db; exit };
$SIG{HUP} = sub { dump_to_file if push_to_db };
Меняем владельца и права доступа:
# sudo chown root:gdk /usr/local/sbin/gdklogd
# sudo chmod 750 /usr/local/sbin/gdklogd
Напишем стартовый сценарий:
# vi /etc/init.d/gdklogd
#!/bin/sh
DAEMON=/usr/local/sbin/gdklogd
DAEMONFLAGS="-D"
KILL=/bin/kill
PID=/var/gdklog/gdklogd.pid
CAT=/bin/cat
SU=/bin/su
start ()
{
echo -n $"Starting $DAEMON: "
# Запуск с правами непривилегированного пользователя
$SU -c "$DAEMON $DAEMONFLAGS" gdk 2>/dev/null 1>&2
}
stop ()
{
echo -n $"Stopping $DAEMON: "
$KILL `$CAT $PID` 2>/dev/null 1>&2
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
stop
start
;;
*)
echo $"Usage: $0 {start|stop|restart}"
exit 1
esac
# sudo chown root:root /etc/init.d/gdklogd
# sudo chmod 700 /etc/init.d/gdklogd
Запустим:
# sudo /etc/init.d/gdklogd start
После запуска сценария с параметром -D он переходит в режим демона, настраивает последовательный порт и начинает считывать из него данные. При запуске без параметра -D сценарий не отсоединяется от терминала, а записи об исходящих звонках дублируются на экран.
Если все сделано правильно, то через некоторое время таблица gdklog начнет заполняться записями.
# sudo kill –HUP `cat /var/gdklog/gdklogd.pid`
# psql -c "SELECT * FROM gdklog;" -U gdk pbxbilling
d_time | station | line | t_call | c_number
---------------------+---------+------+----------+----------
2005-08-21 16:21:00 | 144 | 6 | 00:00:11 | 1234567
(1 запись)
|
Вот и все. При минимуме усилий мы получили вполне работоспособную и переносимую систему учета исходящих звонков УАТС LG GDK-162. Работа скрипта проверялась на Debian GNU/Linux 3.1r Sarg и FreeBSD 5.2.1 (надо изменить только имя файла последовательного порта $ttys). Модуль DBI позволяет использовать любую поддерживаемую им СУБД с минимальной правкой кода, а также доступ к базе данных по сети. Для получения отчетов к нашим услугам вся мощь SQL. Несложно добавить поддержку других моделей УАТС. А при наличии свободного времени можно написать веб-интерфейс.
Успехов!