Все что нам понадобится для отправки 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
Скачать можно по ссылке

Комментарии (22)


  1. Rupper
    28.04.2015 19:25
    +7

    Мы отказались от поддержки модемов году так в 2008-м.
    С тех пор проще и дешевле реализовать работу с SMS через гейты SMPP протоколом.


    1. magnitudo
      29.04.2015 03:24
      +1

      Гейты хорошо, когда есть интернет.
      А когда нужно из автономной системы СМС слать — проще и дешевле модема не найти.


      1. Rupper
        29.04.2015 09:53

        Это да. Я не знаю, какая система у Вас, но может еще стоило написать, что такой сервис должен блокировочку ставить чтобы избежать попытки одновременного доступа из нескольких потоков.


  1. netwer
    28.04.2015 20:39

    Спасибо за статью.
    А почему бы не использовать SMS сервисы, которые предоставляют готовые библиотеки?


    1. asommer Автор
      28.04.2015 21:21

      Всегда пожалуйста!
      А есть бесплатные SMS сервисы? Руководствовался ценой.


      1. netwer
        28.04.2015 21:41

        К сожалению, бесплатных не знаю.


      1. 3dm
        28.04.2015 22:02
        +1

        Так вы ж за смс всё равно платите по тарифам оператора. Правильно?


        1. asommer Автор
          28.04.2015 22:11
          +1

          Правильно. Логичный довод в пользу сервисов. Пусть каждый решает сам для себя.


        1. ringman
          28.04.2015 23:51

          Решал как-то аналогичную задачу. С SMS шлюзами возникает вопрос, как платить, как получать документы, как проводить по бухгалтерии. А с модемом или телефоном проще — статья «расходы на услуги связи», симка резервная тоже имеется.


          1. toper
            29.04.2015 09:30
            +1

            Нам, например, запретили сторонние сервисы привлекать для рассылки сообщений абонентам, ссылаясь на то, что «а вдруг список телефонов утечёт в сеть...» и т.п. Так что использование модемов вполне себе оправдано. Правда в последнее время после принятия закона про смс операторы стали настырно впаривать свои сервисы для корпоративной рассылки смс, аргументируя тем, что номера с которых ведётся рассылка будут однозначно автозабанены другими операторами.


          1. kuber
            29.04.2015 10:46

            >> как платить, как получать документы
            Это вообще не проблема. Ровно также, как вы закупаете бумагу, воду и т.д.

            >> А с модемом или телефоном проще
            Проще, пока смс мало, а когда их количество становится большим или вам становится необходимым принимать смс от пользователя, то все становится сложнее.


      1. vlx
        28.04.2015 22:27

        www.nexmo.com REST API, номера любой страны, все дела. Даже прием SMS зачастую работает, не то что отправка.
        Цены достаточно низкие.


    1. niveus_everto
      28.04.2015 22:50

      Когда СМС отправляется через модем (AT + PDU), можно дополнительно указать «порт» сообщения и в большинстве своем ОСь телефона его проигнорирует, если порт не будет стандартным. Это можно использовать для передачи данных (в том числе шифрованных).


  1. TheTony
    28.04.2015 22:30

    спасибо! Вот только вместо "/r/n" — Вы почему-то не использовали Environment.NewLine… хотя и сослались на нее :-)


    1. niveus_everto
      28.04.2015 22:45
      +3

      Environment.NewLine — ОС зависимая (соответственно — не безопасно использовать для гайдов, которые можно воспроизвести на других ОС), а модемы, с которыми мне приходилось общаться требуют именно \r\n


      1. asommer Автор
        28.04.2015 22:50
        +1

        Да, вы совершенно правы!


    1. asommer Автор
      28.04.2015 22:47

      Делал компиляцию из различных исходников, найденных в сети, а там везде возвраты каретки с помощью управляющих символов. Так и ко мне в код они попали.


  1. smkv
    29.04.2015 01:38

    Вот похожая реализация посылки SMS на Java github.com/smkv/sms-gateway/blob/master/src/ee/smkv/sms/senders/JSSCATSmsSender.java


    1. vlivyur
      29.04.2015 16:59

      smslib тем же самым занимается.


  1. Tujh
    30.04.2015 09:51

    По выбору СОМ-порта, не правильно, у вас сработало из-за модели модема, на других может и не получится. По правильному нужно выбирать порт для 3G Modem, в вашем случае, вероятно будет СОМ6. Смотреть прямо в свойствах модема.


    1. asommer Автор
      30.04.2015 17:21

      Спасибо за правку.


  1. Namelles_One
    30.04.2015 14:08

    За статью — плюс однозначно, велосипед — это всегда полезно, с точки зрения понимания происходящего.

    Но, для реальных рассылок — лучше использовать что-то проверенное временем. Мы — используем Kannel, например.