unix 2010-09-22 12-11-19
Пример реализации отправки и приема 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:
user@machine:~$ echo "Hello from Gnokii!" | gnokii --sendsms номер_телефона
GNOKII Version 0.6.26
Send succeeded!
Телефон, на который было отправлено тестовое сообщение, должен сразу же его получить. К слову, я упустил параметр -r, который включает получение отчета о доставке. Проверим работу в обратную сторону:
user@machine:~$ 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/^+?380d{9}$/)
{
#print "Number is OK, sending SMS...
";
}
else
{
#print "Number doesn't seem to be in international format.
";
if ($number =~ m/^80d{9}$/)
{
#print "OK, number is in national format, restoring it...
";
$number = "+3" . $number;
#print "The number is now " . $number . " and it seems to be OK.
";
}
else
{
print "ERROR: Could not understand the number $number. Check the number and try again.
";
return 1;
}
}
$text = trim($text);
if ($text eq '')
{
print "ERROR: The string is empty!
";
}
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!
";
}
}
# Функция 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:
sendsms destination text
";
if ($numArgs > 2)
{
print "
Error: too many arguments! Looks like you forgot about whitespaces
";
}
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}
";
$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
:
http://www.nixp.ru/articles/%D0%A0%D0%B5%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F-%D0%BE%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%BA%D0%B8-%D0%B8-%D0%BF%D1%80%D0%B8%D1%91%D0%BC%D0%B0-SMS-%D1%81-%D0%BF%D0%BE%D0%BC%D0%BE%D1%89%D1%8C%D1%8E-Gnokii.html