Создаём систему
учета
Денис Соколов
Сегодня мы займемся созданием системы учета исходящих телефонных звонков
на примере УАТС 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 **
Рассмотрим её содержимое:
n первое поле – порядковый номер записи;
n второе – номер станции, с которой сделан вызов;
n третье – номер внешней линии;
n четвертое – длительность звонка (mm:ss);
n пятое – дата;
n шестое – время (hh:mm);
n седьмое – вызванный номер с ведущим символом 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 для
хранения статистики. Таблица имеет пять полей:
n d_time – дата и время;
n station – станция;
n line – внешняя линия;
n t_call – продолжительность разговора;
n 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");
}
Постоянно держать открытым соединение с базой
данных – не очень хорошая идея. Поэтому все строки, соответствующие регулярному
выражению «/O\d/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. Несложно добавить поддержку других моделей УАТС. А при наличии свободного
времени можно написать веб-интерфейс.
Успехов!