Пример реализации отправки и приема SMS-сообщений в Debian GNU/Linux с использованием Gnokii, MySQL и Perl.
В современном мире человек не может представить своё существование без
целого вороха разных высокотехнологичных устройств: начиная от
электронных термометров и заканчивая сложными сетями связи. Для многих
главное место среди всей электроники занимает мобильный телефон.
Действительно, сложно представить себе современного человека, который не
носит с собой постоянно телефон. Очень удобно: мы привыкли к тому, что
нам банки присылают счета, интернет-провайдеры напоминают о
необходимости оплаты, да и мелкие мастерские уже зачастую уведомляют о
завершенном ремонте электроплиты или даже обуви. И всё это нам приходит с
помощью SMS. Почему бы системному администратору не воспользоваться
этим, казалось бы, привычным методом общения в своих целях?
Конечно же, в Сети существует много сервисов, позволяющих это сделать —
чего только стоят скрипты, отсылающие SMS через сайты сотовых
провайдеров. В этой статье я расскажу, как это сделать, используя Debian
GNU/Linux, Gnokii и обычный мобильный телефон, подключенный к серверу
через USB, Bluetooth или последовательный порт. Кроме Gnokii нам
понадобится сервер MySQL (он может находиться и на другой машине), Perl,
Perl-модуль DBI для работы с MySQL в скрипте отправки, а также базовые
навыки работы с MySQL и GNU/Linux. Отличительная особенность данного
решения — полная автономность и независимость от интернет-соединения,
что позволяет строить более комплексные системы (например, аварийную
сигнализацию в случае сбоя оборудования).
Установка, настройка и проверка Gnokii
Процесс установки необходимого ПО мы опустим — это слишком долго, да и
читающие эту статью наверняка смогут выполнить все необходимые действия
самостоятельно. Скажу лишь, что в Debian необходимые пакеты — это
gnokii-smsd-mysql и libdbi-perl вместе с зависимостями.Теперь немного о
Gnokii. Проект изначально предназначался для синхронизации телефонов
Nokia с компьютером, но впоследствии был расширен для работы с другими
устройствами, а также получил много новых применений. Более подробно с
проектом можно ознакомиться на официальном сайте www.gnokii.org/.
После установки необходимо настроить Gnokii путём редактирования файла
/etc/gnokiirc. Процесс настройки очень хорошо описан в wiki.gnokii.org,
но самые главные параметры — это port (для USB — скорее всего
/dev/ttyACM0; COM-порты — /dev/ttyS0 и /dev/ttyS1 для COM1 и COM2
соответственно) и model («symbian» — для Symbian-устройств Nokia;
«6110», «7110», «6510», «3110», «2110», «6160» — для соответствующих
моделей; «AT» — для всех остальных AT-совместимых устройств). У меня,
например, Motorola ROKR Z6, подключенная к USB, и настроено так:
port = /dev/ttyACM0
model = AT
Все остальные параметры оставлены по умолчанию. Замечу, что в
некоторых телефонах нужно выбрать режим соединения «модем» либо
«передача данных», так как с телефоном мы будем работать как с
GSM-модемом, то есть посредством AT-команд. Теперь проверим работу,
выполнив простую команду отправки SMS:
[email protected]:~$ echo «Hello from Gnokii!» | gnokii —sendsms номер_телефона
GNOKII Version 0.6.26
Send succeeded!
Телефон, на который было отправлено тестовое сообщение, должен сразу
же его получить. К слову, я упустил параметр -r, который включает
получение отчета о доставке. Проверим работу в обратную сторону:
[email protected]:~$ gnokii —smsreader
GNOKII Version 0.6.26
Entered sms reader mode…
Отправляем со своего телефона в ответ на тестовое сообщение, которое
должен принять и обработать компьютер. Текст сообщения пока что значения
не имеет — нам важно лишь проверить получение SMS. Видим, что сообщение
получено успешно. Замечу, что на данном этапе сообщение может остаться в
памяти телефона — это зависит от каждого конкретного аппарата. При
использовании smsd оно будет удаляться из памяти телефона и останется
исключительно в базе: этого можно не бояться — даже моя Motorola, часто
уличаемая в некорректном поведении в качестве модема, нормально работала
в такой связке.Самый главный зверь — smsdСледующий
этап — настройка и запуск главной части нашей системы, а именно — демона
smsd, который будет принимать, отправлять и хранить в базе все
сообщения. Создаём на MySQL-сервере базу данных и пользователя для
демона. Я привык делать это через phpMyAdmin, хотя можно воспользоваться
любым удобным вам способом. В базу нужно загрузить структуру таблиц —
SQL-файл находится в системе
(/usr/share/doc/gnokii-smsd-mysql/sms.tables.mysql.sql). Обратите внимание, что в этом дампе есть и создание базы, и
создание учетной записи пользователя базы, поэтому будет логичным
оставить из него лишь нужную часть, включающую в себя только создание
таблиц:
CREATE TABLE inbox (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
number varchar(20) NOT NULL DEFAULT »,
smsdate datetime NOT NULL DEFAULT ‘0000-00-00 00:00:00’,
insertdate timestamp NOT NULL,
text text,
phone tinyint(4),
processed tinyint(4) NOT NULL DEFAULT ‘0’,
PRIMARY KEY (id)
);
CREATE TABLE outbox (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
number varchar(20) NOT NULL DEFAULT »,
processed_date timestamp NOT NULL,
insertdate timestamp NOT NULL,
text varchar(160) DEFAULT NULL,
phone tinyint(4),
processed tinyint(4) NOT NULL DEFAULT ‘0’,
error tinyint(4) NOT NULL DEFAULT ‘-1’,
dreport tinyint(4) NOT NULL DEFAULT ‘0’,
not_before time NOT NULL DEFAULT ’00:00:00′,
not_after time NOT NULL DEFAULT ’23:59:59′,
PRIMARY KEY (id)
);
CREATE TABLE multipartinbox (
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
number varchar(20) NOT NULL DEFAULT »,
smsdate datetime NOT NULL DEFAULT ‘0000-00-00 00:00:00’,
insertdate timestamp NOT NULL,
text text,
phone tinyint(4),
processed tinyint(4) NOT NULL DEFAULT ‘0’,
refnum int(8) DEFAULT NULL,
maxnum int(8) DEFAULT NULL,
curnum int(8) DEFAULT NULL,
PRIMARY KEY(id)
);
У демона нет файла конфигурации — все настройки для работы с базой
задаются в командной строке. Работу с телефоном мы уже настроили и
проверили — настройки читаются из /etc/gnokiirc. Попробуем запустить:
sudo /usr/sbin/smsd -u db_username -d db_name -p db_password \
-c db_hostname -m mysql -b SM -f /var/log/smsdaemon
Используемые здесь параметры db_username, db_name, db_password,
db_hostname — это соответственно имя пользователя, имя базы, пароль и
адрес сервера СУБД (у меня это был localhost). SM — это тип памяти, в
которой хранятся SMS (в данном случае — Sim Memory); его значение
подбирается скорее экспериментальным путём: в каждой конкретной модели
телефона они могут быть перепутаны местами либо названы по-другому
(полный список возможных значений можно найти наwiki.gnokii.org/index.php/Memory_type_codes).
Здесь стоит заметить, что smsd работает немного по принципу,
отличающему его от gnokii: gnokii просит телефон передавать ему все
входящие SMS (из-за этого и неразбериха: некоторые телефоны сохраняют
после этого входящие сообщения, некоторые — нет), а smsd проверяет
наличие новых сообщений в памяти телефона, после чего принятые сообщения
читаются и удаляются. Такой принцип работы отличается надежностью:
например, телефон можно перезагрузить либо временно отключить — все
принятые за это время SMS всё равно обработаются сервером.
После запуска мы должны увидеть в консоли сообщения об успешном
подключении телефона, получении/приёме SMS и так далее. Если все
работает, можно завершать процесс нажатием на Ctrl+c.Для запуска сервера
воспользуемся init-скриптом, приложенным к статье (см. в самом низу) и
написанным мною (точнее, отредактированным из примера). Важно не забыть
сменить значение DAEMON_ARGS на свои параметры. Двоеточие в конце файла —
это часть init-скрипта, сбрасывающая значение кода возврата.
Почти готово: осталось записать всё это в /etc/init.d/smsd, сделать файл
исполняемым, добавить его в загрузку и запустить. Последнее в Debian
делается так:
update-rc.d smsd defaults
invoke-rc.d smsd start
Теперь smsd настроен, связь работает — можно начинать пожинать плоды своего труда.
Когда самое сложное позади…
Принцип действия smsd прост: всё, что приходит, — записываем в таблицу
inbox (важно: значение processed устанавливается равное нулю), а всё,
что есть в таблице outbox с processed, равным нулю (т.е. не обработано),
— отсылаем и ставим processed = 1. Не путайте таблицы inbox и outbox — у
них обеих есть это поле. Интересно заметить, что в inbox это поле
совсем не используется демоном — оно оставлено специально для внешних
обработчиков. Мы его используем в обработке входящих SMS для обозначения
уже обработанных сообщений. Для отправки SMS мною написан небольшой
модуль на Perl. trim($) добавлен сюда же просто ради удобства — мы его
всё равно потом будем использовать. Модуль следует обозвать Sms.pm, а в
Debian Lenny записать в /usr/local/lib/perl/5.10.0 (в других
дистрибутивах — по вкусу). Важно заметить, что здесь используется фильтр
номеров для Украины (для России понадобится небольшая доработка).
#!/usr/bin/perl
# Sendsms for Gnokii
use DBI;
# конфигурация для MySQL:
$mysqlHost = ‘db_hostname’;
$mysqlDb = ‘db_name’;
$mysqlUser = ‘db_username’;
$mysqlPassword = ‘db_password’;
$mysqlConnect = «dbi:mysql:$mysqlDb;$mysqlHost»;
sub sendsms {
my $number = @_[0];
my $text = @_[1];
if ($number =~ m/^\+?380\d{9}$/)
{
#print «Number is OK, sending SMS…\n»;
}
else
{
#print «Number doesn’t seem to be in international format.\n»;
if ($number =~ m/^80\d{9}$/)
{
#print «OK, number is in national format, restoring it…\n»;
$number = «+3» . $number;
#print «The number is now » . $number . » and it seems to be OK.\n»;
}
else
{
print «ERROR: Could not understand the number $number. Check the number and try again.\n»;
return 1;
}
}
$text = trim($text);
if ($text eq »)
{
print «ERROR: The string is empty!\n»;
}
else
{
my $dbh = DBI->connect($mysqlConnect, $mysqlUser, $mysqlPassword);
my $query = «INSERT INTO `outbox` (`number`,`text`) VALUES (‘$number’, ‘$text’);»;
my $sth = $dbh->prepare($query);
$sth->execute();
$sth->finish();
$dbh->disconnect;
#print «OK, message sent. See you later!\n»;
}
}
# Функция trim для устранения пробелов в начале и конце строки
sub trim($)
{
my $string = shift;
$string =~ s/^\s+//;
$string =~ s/\s+$//;
return $string;
}
Пример использования — простая отправка SMS:
#!/usr/bin/perl
# Sendsms for Gnokii
use Sms;
$numArgs = $#ARGV + 1;
if ($numArgs != 2)
{
print «Usage:\n\tsendsms destination text\n»;
if ($numArgs > 2)
{
print «\nError: too many arguments! Looks like you forgot about whitespaces\n»;
}
exit 1;
}
else
{
$number = $ARGV[0];
$text = $ARGV[1];
&sendsms ($number,$text);
}
Теперь заставим сервер отвечать на SMS:
#!/usr/bin/perl
use DBI;
use Sys::Hostname;
use Sms;
# Конфигурация для MySQL:
$mysqlHost = ‘db_hostname’;
$mysqlDb = ‘db_name’;
$mysqlUser = ‘db_username’;
$mysqlPassword = ‘db_password’;
$mysqlConnect = «dbi:mysql:$mysqlDb;$mysqlHost»;
$dbh = DBI->connect($mysqlConnect, $mysqlUser, $mysqlPassword);
$query = «SELECT `id`, `number`, `text` FROM `inbox` WHERE `text` REGEXP ‘^».hostname.»*’ AND `processed` = 0;»;
while (true) {
$sth = $dbh->prepare($query);
$results = $dbh->selectall_hashref($query, ‘id’);
foreach my $id (keys %$results) {
print «Value of ID $id is $results->{$id}->{text}\n»;
$hostname = hostname;
$text = $results->{$id}->{text};
if ($text =~ m/^$hostname/)
{
if ($text =~ m/^$hostname hello/)
{
sendsms($results->{$id}->{number}, «Hello to you from $hostname!»);
}
if ($text =~ m/^$hostname uptime/)
{
$message = «$hostname uptime:».qx (‘uptime’);
sendsms($results->{$id}->{number},$message);
}
$dbh->do(«UPDATE `inbox` SET `processed` = ‘1’ WHERE `id` = $id LIMIT 1 ;»);
}
}
sleep(1);
}
$dbh->disconnect();
Запускаем. Если отправить серверу SMS с текстом «имя_сервера uptime»,
в ответ придет время работы сервера. На запрос же «имя_сервера hello»
детище поприветствует своего хозяина. Простора для творчества здесь
много. Единственное условие — желательно, чтобы первое слово всё равно
было именем сервера — ведь обработчик может быть запущен на нескольких
машинах, а значит — SMS можно адресовать на любой из них.Вместо заключенияВнимательные
читатели могли заметить, что нам никто и ничто не мешает запустить
несколько обработчиков входящих сообщений на нескольких серверах (и,
соответственно, слать SMS тоже с нескольких серверов) — ведь мы работаем
уже не с телефоном (с ним работает smsd), а с базой. Также предложенную
мной схему работы можно немного улучшить — например, посредством udev
запускать smsd при подключении телефона или останавливать после
отключения (работать, правда, это будет только с USB). Дело техники —
сделано, дальше — лишь полёт фантазии. К примеру, я делаю рассылки с
напоминаниями своим клиентам, а мой zabbix-сервер оповещает меня о
проблемах. Всё в ваших руках!
#!/bin/sh
### BEGIN INIT INFO
# Provides: smsd
# Required-Start: $remote_fs
# Required-Stop: $remote_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Gnokii SMSD
# Description: This is SMSD startup script
### END INIT INFO
# Author: Vadim Abramchuk
#
# Do NOT «set -e»
# PATH should only include /usr/* if it runs after the mountnfs.sh script
PATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC=»Gnokii SMS daemon»
NAME=smsd
DAEMON=/usr/sbin/$NAME
DAEMON_ARGS=»-u db_username -d db_name -p db_password -c db_hostname -m mysql -b SM -f /var/log/smsdaemon»
PIDFILE=/var/run/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME
# Exit if the package is not installed
[ -x «$DAEMON» ] || exit 0
# Read configuration variable file if it is present
[ -r /etc/default/$NAME ] && . /etc/default/$NAME
# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh
VERBOSE=on
# Define LSB log_* functions.
# Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
. /lib/lsb/init-functions
#
# Function that starts the daemon/service
#
do_start()
{
# Return
# 0 if daemon has been started
# 1 if daemon was already running
# 2 if daemon could not be started
start-stop-daemon -b -m —start —quiet —pidfile $PIDFILE —exec $DAEMON —test > /dev/null \
|| return 1
start-stop-daemon -b -m —start —quiet —pidfile $PIDFILE —exec $DAEMON — \
$DAEMON_ARGS \
|| return 2
# Add code here, if necessary, that waits for the process to be ready
# to handle requests from services started subsequently which depend
# on this one. As a last resort, sleep for some time.
}
#
# Function that stops the daemon/service
#
do_stop()
{
# Return
# 0 if daemon has been stopped
# 1 if daemon was already stopped
# 2 if daemon could not be stopped
# other if a failure occurred
start-stop-daemon —stop —quiet —retry=TERM/30/KILL/5 —pidfile $PIDFILE —name $NAME
RETVAL=»$?»
[ «$RETVAL» = 2 ] && return 2
# Wait for children to finish too if this is a daemon that forks
# and if the daemon is only ever run from this initscript.
# If the above conditions are not satisfied then add some other code
# that waits for the process to drop all resources that could be
# needed by services started subsequently. A last resort is to
# sleep for some time.
start-stop-daemon —stop —quiet —oknodo —retry=0/30/KILL/5 —exec $DAEMON
[ «$?» = 2 ] && return 2
# Many daemons don’t delete their pidfiles when they exit.
rm -f $PIDFILE
return «$RETVAL»
}
#
# Function that sends a SIGHUP to the daemon/service
#
do_reload() {
#
# If the daemon can reload its configuration without
# restarting (for example, when it is sent a SIGHUP),
# then implement that here.
#
start-stop-daemon —stop —signal 1 —quiet —pidfile $PIDFILE —name $NAME
return 0
}
case «$1» in
start)
[ «$VERBOSE» != no ] && log_daemon_msg «Starting $DESC» «$NAME»
do_start
case «$?» in
0|1) [ «$VERBOSE» != no ] && log_end_msg 0 ;;
2) [ «$VERBOSE» != no ] && log_end_msg 1 ;;
esac
;;
stop)
[ «$VERBOSE» != no ] && log_daemon_msg «Stopping $DESC» «$NAME»
do_stop
case «$?» in
0|1) [ «$VERBOSE» != no ] && log_end_msg 0 ;;
2) [ «$VERBOSE» != no ] && log_end_msg 1 ;;
esac
;;
#reload|force-reload)
#
# If do_reload() is not implemented then leave this commented out
# and leave ‘force-reload’ as an alias for ‘restart’.
#
#log_daemon_msg «Reloading $DESC» «$NAME»
#do_reload
#log_end_msg $?
#;;
restart|force-reload)
#
# If the «reload» option is implemented then remove the
# ‘force-reload’ alias
#
log_daemon_msg «Restarting $DESC» «$NAME»
do_stop
case «$?» in
0|1)
do_start
case «$?» in
0) log_end_msg 0 ;;
1) log_end_msg 1 ;; # Old process is still running
*) log_end_msg 1 ;; # Failed to start
esac
;;
*)
# Failed to stop
log_end_msg 1
;;
esac
;;
*)
#echo «Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}» >&2
echo «Usage: $SCRIPTNAME {start|stop|restart|force-reload}» >&2
exit 3
;;
esac
: