Как стать автором
Обновить

Отправляем SMS из .Net приложения на C#

Время на прочтение 7 мин
Количество просмотров 61K

Все что нам понадобится для отправки SMS это 3G USB модем, SIM карта, Visual Studio и немного времени. Моей целью не является описать все возможные настройки COM порта или формата PDU. Я хотел бы предоставить вам готовое решение, которое можно использовать в качестве сниппета в своих проектах.
В качестве примера рассмотрим 2 консольных приложения. Почему консольных? Да потому, что в них нет ничего лишнего и проще разобрать код. Почему два? Потому, что есть два распространенных варианта отправки сообщений. Самый простой вариант – это отправка сообщений в текстовом режиме. Минусом этого варианта является то, что он не поддерживает отправку кириллицы. Плюсом то, что возможна отправка 160-ти символов. Второй вариант, более сложный, позволяет отправлять текст длиной до 70-ти символов в формате Unicode.

Отправка сообщений в режиме текста.


Итак, создаем консольное приложение и, перво-наперво, добавляем пространство имен:
using System.IO.Ports;

Далее, объявим переменную типа SerialPort
static SerialPort port;

Она у нас static потому, что приложение консольное.

Внутри основной процедуры приложения static void Main(string[] args) инициализируем переменную с помощью конструктора new SerialPort()
port = new SerialPort();

Вынесем установку настроек порта и его открытие в отдельную процедуру:
   private static void OpenPort()
        {
            port.BaudRate =2400; // еще варианты 4800, 9600, 28800 или 56000
            port.DataBits = 7; // еще варианты 8, 9

            port.StopBits = StopBits.One; // еще варианты StopBits.Two StopBits.None или StopBits.OnePointFive         
            port.Parity = Parity.Odd; // еще варианты Parity.Even Parity.Mark Parity.None или Parity.Space

            port.ReadTimeout = 500; // самый оптимальный промежуток времени
            port.WriteTimeout = 500; // самый оптимальный промежуток времени

            port.Encoding = Encoding.GetEncoding("windows-1251");
            port.PortName = "COM5";

            // незамысловатая конструкция для открытия порта
            if (port.IsOpen) 
                    port.Close(); // он мог быть открыт с другими параметрами
            try
            {
                port.Open();
            }
            catch (Exception e) { }
        }

Настройки могут быть специфичны для различных модемов. Я проводил тесты на модели Huawei E150, и при этом отправка совершалась практически при любых настройках.
Все настройки доступны в MSDN по этой ссылке.
Номер порта определяется с помощью диспетчера устройств. В следующем примере это порт COM4:

После того, как порт открыт, можно приступить к отправке сообщения с помощью AT команд.
Посылаем команду модему с помощью port.WriteLine. Между отправками команд обязательно делаем паузу — System.Threading.Thread.Sleep(500)

Отправим первые две команды:
  port.WriteLine("AT \r\n"); // значит Внимание! для модема 
  System.Threading.Thread.Sleep(500);
  port.Write("AT+CMGF=1 \r\n"); // устанавливается текстовый режим для отправки сообщений
  System.Threading.Thread.Sleep(500);

Первая команда просит модем перейти в режим готовности. Вторая команда задает текстовый режим отправки сообщений. Обратите внимание, что после команд идут символы переноса строки \r\n. В различных примерах отправки сообщений, которые можно найти в сети, указываются различные переносы. Где-то только \r, где-то \n, а бывает даже и \n\r. По официальной информации с MSDN:
Environment.NewLine это строка, содержащая "\r\n" для платформ, отличных от Unix, или строка, содержащая "\n" для платформ Unix. То есть в нашем случае используем \r\n.

Для упрощения, отключим на SIM карте проверку PIN кода. Это можно сделать как с помощью приложения поставляемого с модемом, так и с помощью мобильного телефона.
Номер телефона, который будет передан модему в качестве параметра команды, не должен содержать никаких символов, кроме цифр и знака «+». То есть, он должен быть передан в международном формате. Отправляем команду с номером телефона получателя СМС:
port.Write("AT+CMGS=\"+375123456789\"" + "\r\n");
System.Threading.Thread.Sleep(500);

И далее отправляем сам текст сообщения:
port.Write("Hello from modem!" + char.ConvertFromUtf32(26) + "\r\n");
System.Threading.Thread.Sleep(500);

Обратите внимание, что после текста сообщения передается символ char.ConvertFromUtf32(26)
При посылке АТ команд, именно после передачи самого текста сообщения, необходимо передать комбинацию CTRL-Z. Эта комбинация и есть 26-ой символ UTF32.
После того, как сообщение отправлено, правильным было бы закрыть порт
port.Close();

После передачи команды модем, как правило, дает свой ответ с подтверждением или ошибкой. Считать эти данные можно с помощью метода port.ReadExisting(), возвращающего строковое значение ответа модема.
В процедуру открытия порта можно добавить регистрацию события получения портом данных:
port.DataReceived += SerialPortDataReceived;

И далее реализовать обработку данных:
     private void SerialPortDataReceived(object sender, SerialDataReceivedEventArgs e)
        {
// считывать ответы порта можно и здесь
        }

Программа работает в одном потоке. С COM портами даже рекомендуется работать в одном потоке. Для этого можно пометить класс/код атрибутом [STAThread]. Если кто-то в курсе подробностей по поводу многопоточной работы с портами COM, буду рад комментариям.

Ссылка на github
Скачать можно по ссылке

Отправка сообщения в виде текста является довольно популярной темой в сети. И если с отправкой текста все понятно, то при отправке сообщений в режиме PDU возникают вопросы, которые не всегда раскрыты.

Отправка сообщений в режиме PDU (Protocol data unit)


Открытие порта совершенно идентично открытию порта из вышеописанного примера.
Первые две команды также аналогичны прошлому примеру за исключением того, что во второй команде параметром передается 0.
port.WriteLine("AT\r\n"); // означает "Внимание!" для модема 
System.Threading.Thread.Sleep(500);

port.Write("AT+CMGF=0\r\n"); // устанавливается цифровой режим PDU для отправки сообщений
System.Threading.Thread.Sleep(500);

При отправке сообщения в виде PDU номер мобильного телефона не должен содержать ничего кроме цифр. Если число цифр нечетное, то необходимо добавить в конец номера F. Плюс ко всему соседние цифры номера должны быть переставлены местами. Например, номер получателя SMS 1234567, число цифр нечетное, а значит, добавляем F. Делаем перестановку и получаем 214365F7.
Сделать это преобразование нам поможет следующая функция:
  // перекодирование номера телефона для формата PDU
        public static string EncodePhoneNumber(string PhoneNumber)
        {
            string result = "";
            if ((PhoneNumber.Length % 2) > 0) PhoneNumber += "F";

            int i = 0;
            while (i < PhoneNumber.Length)
            {
                result += PhoneNumber[i + 1].ToString() + PhoneNumber[i].ToString();
                i += 2;
            }
            return result.Trim();
        }

Само сообщение также необходимо закодировать в формат UCS2. Этот формат является устаревшей версией формата UTF-16. Отличие заключается в том, что UCS2 не может быть использован для совместимого представления дополнительных символов. Ссылка на FAQ
Проще выражаясь: коды UTF-16 и UCS2 большей частью совпадают.
Код символа можно посмотреть либо проверить по таблице символов, которая находится в меню Пуск — Все программы – Стандартные – Служебные — Таблица символов

Код для буквы «е» — 0435
В некоторых примерах из сети, преобразование буквы в код происходит через сопоставление буквы с кодом. То есть создается массив с буквами и массив с соответствующими буквам кодами. Каждая буква текста сообщения заменяется кодом. Этот пример работает, но я предпочел другой:
// перекодирование текста смс в UCS2 
        public static string StringToUCS2(string str)
        {
            UnicodeEncoding ue = new UnicodeEncoding();
            byte[] ucs2 = ue.GetBytes(str);

            int i = 0;
            while (i < ucs2.Length)
            {
                byte b = ucs2[i + 1];
                ucs2[i + 1] = ucs2[i];
                ucs2[i] = b;
                i += 2;
            }
            return BitConverter.ToString(ucs2).Replace("-", "");
        }

Казалось бы, все относительно просто, но не тут-то было! Вместе с зашифрованным номером телефона и сообщением нам необходимо передать еще кучу информации.

Допустим, у нас есть строковая переменная telnumber с номером телефона (только цифры без символа «+» в международном формате). Формируем код для телефонного номера:
telnumber = "01"+"00" + telnumber.Length.ToString("X2") + "91" + EncodePhoneNumber(telnumber);

«01» это PDU Type или иногда называется SMS-SUBMIT. 01 означает, что сообщение передаваемое, а не получаемое
«00» это TP-Message-Reference означают, что телефон/модем может установить количество успешных сообщений автоматически
telnumber.Length.ToString(«X2») выдаст нам длинну номера в 16-ричном формате
«91» означает, что используется международный формат номера телефона
Ну, а EncodePhoneNumber это функция, упомянутая выше.

Теперь возьмем переменную textsms с текстом сообщения и получим код самого сообщения:
textsms = StringToUCS2(textsms); 

Далее нам нужно получить длину этого сообщения в 16-ричном формате. Получим ее так:
string leninByte = (textsms.Length / 2).ToString("X2");


Объединим код нашего номера телефона, длину сообщения, код сообщения и добавим ко всему этому промежуточные кодовые символы:
textsms = telnumber + "00"+"0"+"8" + leninByte + textsms;

«00» означает, что формат сообщения неявный. Это идентификатор протокола. Другие варианты телекс, телефакс, голосовое сообщение и т.п.
«0» если вместо 0 указать 1, то сообщение не будет сохранено на телефоне. Получится всплывающее flash сообщение.
«8» означает формат UCS2 — 2 байта на символ.

Ранее мы уже считали длину номера телефона и текста сообщения. Теперь нам нужно в третий раз подсчитать длину на этот раз уже всего сообщения PDU. Эту длину мы пошлем с АТ командой:
double lenMes = textsms.Length / 2; // получаем количество октет в десятичной системе
port.Write("AT+CMGS=" + (Math.Ceiling(lenMes)).ToString() + "\r\n");
System.Threading.Thread.Sleep(500);

Во второй строчке используется округление в большую сторону. Например, даже если у нас получится 15,5 октета, то передаем мы данные пакетом из 16-ти октет.

Заканчивается этот ужасно запутанный код PDU тем, что нам необходимо передать номер sms-центра нашего оператора связи. Так как почти все SIM карты в наше время уже содержат в себе «зашитый» номер центра, то вместо этого номера передадим код «00», который будет означать, что номер следует взять автоматически из данных SIM карты.
Этот код «00» должен стоять в самом начале сообщения. Как ни странно, хоть эти символы и находятся в самом начале сообщения, но добавлять их нужно на последнем шаге. Причина кроется в том, что нам необходимо было подсчитывать длину сообщения без учета этого кода.
textsms = "00" + textsms;

Все. Теперь можно отправлять код PDU AT командой:
port.Write(textsms + char.ConvertFromUtf32(26) + "\r\n"); // опять же с комбинацией CTRL-Z на конце
System.Threading.Thread.Sleep(500);

Ссылка на github
Теги:
Хабы:
+16
Комментарии 22
Комментарии Комментарии 22

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн