Disclaimer

Этот блог появился как реплика с http://blogs.technet.com/tail в силу определенных обстоятельств. Любая информация в ЭТОМ блоге предоставляется без каких либо гарантий и обязательств. Все мнения принадлежат их авторам и не могут быть связаны с позициями и мнениями официальных лиц или организаций.

вторник, 3 июля 2012 г.

Отправка SMS из приложения при помощи GSM-модема – пример кода на C#

UPD: За давностью лет архив с исходными кодами утрачен, поэтому ссылки на него из поста удалены, писать мне просьбы прислать их тоже занятие бесперспективное. Я сожалею, но восстанавливать отдельно не буду.

UPD2: Сей проект впоследствии получил новую жизнь - когда нам срочно надо было придумать механизм уведомления внешних пользователей о номере заявки и ее статусе во время первого Гран-при Формулы 1 в России в 2014 году
< UPD3: Написано это почти 5 лет назад - за это время никто не следил пристально за тем, менялись ли стандарты и RFC на соответствующие технологии... 

В одном из проектов встала задача – отправка срочных уведомлений пользователям нашей системы в виде SMS-сообщений на телефон. Первоначально предполагалось использовать возможности имеющегося у заказчика Cisco Call Manager-а, но оказалось, что в текущей конфигурации эта feature не доступна. Простое решение – использовать GSM-модем.
Сразу оговорюсь – это решение подходит в ситуации, когда количество отправляемых сообщений не превысит 30 штук в минуту. Да, по сути, больше-то вряд ли получится. В нашей системе этот порог достижим едва ли, но, тем не менее, коллеги предложили организовать FIFO-очередь сообщений “на всякий случай”. Подумайте и вы на этот счет…
Далее – мой опыт разбора проблемы и решения поставленной задачи. Местами с пояснениями теоретического и практического толка…
Как раз сразу немного теории. Принципиально, способов, точнее, режимов отправки SMS существует два – text mode и PDU mode. Первый – текстовый режим – простой, без контроля доставки сообщения и с ограничением в 140 символов на текст сообщения с использованием латиницы, соответственно, для кириллицы (оная кодируется в unicode двумя байтами на символ) – всего 70. Также в text mode нет возможности разбивать и склеивать более длинные сообщения. Второй – PDU mode – режим, поддерживающий и прозрачную конкатенацию сообщений, и контроль доставки, и еще ряд фич. По сути, PDU или Packet Data Unit (единица пакета данных или как-то так) это просто текстовая строка, передаваемая модему в шестнадцатиричном виде и содержащая в себе все параметры сообщения.
Принципиально для нашей задачи было отправлять уведомления на русском языке. Беглое изучение шишек, набитых другими, показало, что в text mode это, якобы, невозможно и нужно использовать только PDU mode. И это как в той поговорке, когда “дурная голова рукам покоя не дает”, ибо если внимательно читать документацию, выясняется довольно быстро, что отправка SMS в text mode c использованием кириллицы – это дело двух правильно переданных команд модему и одной матрицы перекодировки в UC2 (он же = хорошо известный unicode). Резюме – внимательное чтение спецификаций занимает чуть больше времени, но стоит того – не придется, к примеру, перепаковывать номер получателя сообщения “странным образом” (связано со спецификой мобильных процессоров, скорее всего): убрать все, кроме цифр, посчитать и к нечетному количеству цифр в конец дописать “F”, разбить получившуюся строку на пары и в оных парах поменять местами цифры. И это еще не самое “прикольное”, поверьте мне на слово или поищите примеры кода отправки SMS в PDU mode…
Итогом всех моих изысканий на тему отправки SMS-сообщений при помощи GSM-модема стало написанное почти на коленке Windows Form-приложение с примером реализации отправки SMS, который я набросала для своих коллег в демонстрационно-воспитательных целях. Я постаралась прокомментировать код так, чтобы было понятно, что происходит. И последовательность отправки команд модему должна быть такой, как идет в проекте.
Что нужно знать про данный пример приложения еще:
  • Отлов и обработка всех возможных exceptions и проверка ответов модема – на вашей совести, а у меня это часть воспитательного процесса…
  • Текущая версия отправляет сообщения на русском языке в text mode (см. комментарии в коде).
  • Все вопросы по коду можете писать мне, но помните, В ЭТОМ ПРОЕКТЕ – ОБРАЗЕЦ КОДА, ДЕМОНСТРИРУЮЩИЙ ОСНОВНОЙ ПРИНЦИП ОТПРАВКИ SMS ПОСРЕДСТВОМ GSM-модема!
  • Проект создавался в VS2010 и допиливался уже в VS2012RC
Что Вам потребуется, если нужно решить такую же задачу:
  • GSM-модем, поддерживающий отправку SMS (в нашем случае это Huawei E1550 от МТС, проверяется эта возможность отправкой команды AT+CSMS=0 вашему модему – если модем ответит OK, то это работает, если ERROR или что-то еще, кроме OK, – меняйте модем или прошивку, что тоже возможно), можно использовать “старую нокию, которая есть у всех”, но будьте готовы к тому, что она будет работать только с PDU mode (AT+CMGF=1 возвращает ошибку). Несмотря на то, что “втыкается” обычно такой модем в разъем USB, в системе он “представляется” COM-портом с номером N. И в один момент открыть этот порт может только один процесс (идея с очередью сообщений все еще не кажется вам достойной внимания? Ну-ну…)
  • сим-карта, естественно, с неотрицательным балансом или с кредитной системой расчета (в нашем случае – это одна из сим-карт, ассоциированных с корпоративным тарифом)
  • опционально, USB-удлинитель, если в Вашей серверной не очень уверенно принимается сигнал оператора сотовой связи – модем, естественно, должен быть подключен напрямую к серверу, где будет выполняться наше приложение
  • на время экспериментов запаситесь заранее последней прошивкой для вашего модема – не повредит, честное слово, найти ее можно, как правило, на сайте производителя или оператора, который вам этот модем продал - при неосторожном обращении с записью/чтением в регистры модема командой AT+CSCA? и AT+CSCA=”номер SMS-центра” – после этого нужно только перепрошивать модем
  • запаситесь также программой-терминалом, умеющей подключаться к COM-порту (например, PuTTY - http://www.chiark.greenend.org.uk/~sgtatham/putty/), пригодится в процессе чтения спецификаций, о которых – ниже. Подключиться к COM-порту из PuTTY можно так:
image
Что прочитать:
  • в первую очередь, стоит внимательно прочитать соответствующий раздел спецификации AT-команд работы с GSM-модемом вообще и AT-команд отправки (и приема, если хотите) SMS-сообщений – текущая версия стандарта (хвала Магистрам) вышла еще в 1998 году и после 2003 года не обновлялась: Digital cellular telecommunications system (Phase 2+);AT Command set for GSM Mobile Equipment (ME) (3GPP TS 07.07 version 7.8.0 Release 1998)
  • Еще одна спецификация AT-команд для отправки SMS - http://www.cellular.co.za/sms_at_commands.htm - там есть пример, кстати, последовательности команд для приема-отправки SMS. И там же есть расшифровка кодов ошибок при отправке SMS через GSM-модем.
Итак, приступим. После чтения спецификаций у вас должна сложиться вот такая последовательность AT-команд для отправки сообщений на русском языке в text mode:
AT // инициализация модема, в ответ должно прийти OK
AT+CSMS=0 //проверяем возможность отправки SMS при помощи данного устройства
AT+CMGF=1 //выбираем режим работы модема - text mode (0 - PDU mode, по умолчанию)
AT+CSCA? //запрашиваем у модема номер SMS-центра, устанавливать его НЕ НАДО 
AT+CSMP=17,167,0,25 //устанавливаем параметры модема для отправки SMS на русском языке 
AT+CSCS="UCS2" //определяем кодировку сообщений в UCS2 
AT+CMGS=”<номер получателя>” //отправили номер телефона получателя (не забудьте в коде его также конвертировать в UCS2
> текст сообщения //отправляем модему текст сообщения, завершив его последовательностью Ctrl+Z (^Z) или 0x1Ah 


Теперь о том, как это делать программно. В C# есть специальный класс в System.IO.Ports для обмена данными с COM-портом – SerialPort. Свойства и методы этого класса позволяют настраивать параметры модема и передавать/получать команды (данные). Чтобы использовать этот класс в своем приложении, объявляем:

using System.IO.Ports;
using System.Threading; //между операциями отправки команды и чтения ответа нужно “выдерживать паузу” посредством System.Threading.Thread.Sleep()


Создаем экземпляр класса для работы с нашим устройством:

_serialPort = new SerialPort();


Метод GetPortNames() возвращает все доступные COM-порты (доступ к этому методу в экземпляре класса, кстати, невозможен):

SerialPort.GetPortNames();


С использованием этого метода, например, можно заполнить элементы ListBox-а COMPorts, в котором будем выбирать нужный нам порт (на форме):

foreach (string s in SerialPort.GetPortNames())
   {
      COMPorts.Items.Add(s);
   }


Открыть порт (как я писала выше, открыть порт можно один раз в единицу времени, забудете закрыть – не сможете открыть повторно) можно, используя свойство PortName и метод Open(), результат которого возвращается в свойство IsOpen:

serialPort.PortName = COMPorts.SelectedItem.ToString(); //устанавливаем свойство PortName

serialPort.Open();
if (serialPort.IsOpen)
   {

      // здесь будем дальше обмениваться данными с устройством

   }


Отправка команды модему (не забываем экранировать кавычки и каждую команду завершать “переводом строки”):

serialPort.Write(comm + "\r\n"); //comm – строка с AT-командой


Чтение данных из модема:

serialPort.ReadExisting(); //читает все доступные байты из потока и входного буфера объекта System.IO.Ports.SerialPort или 
serialPort.Read(char[] buffer, int offset, int count); //читает из входного буфера определенное число символов, не самый удобный способ или 
serialPort.ReadLine(); //считывает все данные из входного буфера до System.IO.Ports.SerialPort.NewLine


Я пользовалась методом ReadExisting(), так как он позволяет прочитать ВСЕ, что находится в потоке/буфере и легко используется для диагностики и отладки. В принципе, для приложения, работающего в фоне, лучше использовать или Read() или ReadLine().

Это все хорошо, но как вы помните, нам нужно текст сообщения на русском языке и номер отправителя сконвертировать в UCS2. Как вариант, можно использовать вот такую функцию:

public string txtInUCS2 = "";
private void ConvertRusToUCS2(string txtInRus)
     {
         //строка с алфавитом
         String strAlphabet  = "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЪЭЮЯабвгдеёжзийклмнопрстуфхцчшщэюяABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'-* :;)(.,!=_";
         //массив для конвертирования русских букв и цифр в UCS2 
         String[] ArrayUCSCode = new String[137]{            
         "0410","0411","0412","0413","0414","0415","00A8","0416","0417",
         "0418","0419","041A","041B","041C","041D","041E","041F","0420",
         "0421","0422","0423","0424","0425","0426","0427","0428","0429",
         "042C","042A","042D","042E","042F","0430","0431","0432","0433",
         "0434","0435","00B8","0436","0437","0438","0439","043A","043B",
         "043C","043D","043E","043F","0440","0441","0442","0443","0444",
         "0445","0446","0447","0448","0449","044D","044E","044F","0041",
         "0042","0043","0044","0045","0046","0047","0048","0049","004A",
         "004B","004C","004D","004E","004F","0050","0051","0052","0053",
         "0054","0055","0056","0057","0058","0059","005A","0061","0062",
         "0063","0064","0065","0066","0067","0068","0069","006A","006B",
         "006C","006D","006E","006F","0070","0071","0072","0073","0074",
         "0075","0076","0077","0078","0079","007A","0030","0031","0032",
         "0033","0034","0035","0036","0037","0038","0039","0027","002D",
         "002A","0020","003A","003B","0029","0028","002E","002C","0021",
         "003D","005F"};
         StringBuilder UCS = new StringBuilder(txtInRus.Length);
         Int32 intLetterIndex = 0;
         for (int i = 0; i < txtInRus.Length; i++)
            {
                intLetterIndex = strAlphabet.IndexOf(txtInRus[i]);
                if (intLetterIndex != -1)
                   {
                       UCS.Append(ArrayUCSCode[intLetterIndex]);
                   }
             }
            txtInUCS2 = UCS.ToString();
     }




Собственно, и все. Особо внимательные могут найти небольшой “баг” в приведенной выше функции…

image



24 комментария:

Unknown комментирует...
Этот комментарий был удален администратором блога.
Unknown комментирует...

Доброго времени суток. У меня возникли вопросы по теме. Отправляю смс в режиме pdu, проходит какое-то время и модем виснет, приходится вручную вытаскивать его из компа и подключать снова. Что может быть причиной? Подскажите, как добиться его стабильной работы? пробовал решить проблему извращенным способом: перезагружать приемник/передатчик после отправки каждой смс, результата не вышло, только появилась катастрофическая задержка между получением, обработкой и отправкой ответной смс, особенно когда их много. Мыслей по этому поводу уже нет. ВЗЫВАЮ И МОЛЮ О ПОМОЩИ.

tails-up комментирует...

Ну, вот, к сожалению принципиально чем-то помочь не смогу, наверное. Кроме того, что я бы попробовала поменять модем - может, это глюк конкретного модема. А вот если будет повторяться на других модемах - тогда сложнее. Тогда надо смотреть, что творится в регистрах модема непосредственно перед зависанием - логировать все, до чего дотянетесь...
И, кстати, как по мне - если ваши сообщение не длиннее 70 символов, то их можно отправлять в text mode на русском языке :)

Unknown комментирует...

Подскажите, для отправки смс, вы открываете порт и сразу отправляете команды для отправки (перевод в нужный режим, отправка смс и тд)? или что-то еще? просто я прочитал, что надо еще включать приемник/передатчик модема, вот думаю, может из-за этого модем галит...

Unknown комментирует...
Этот комментарий был удален автором.
razor комментирует...

Попробовал скомпилировать ваш код. Все отлично, спасибо за исчерпывающую информацию. Есть только одна проблема. Первая СМСка отлично отправляется, а на второй программа начинает кушать память ведрами, полностью грузить проц на 50% и зависает напрочь. И модем отваливается от системы. Где-то в районе конвертации в UCS2. Как думаете, в чем может быть проблема.

tails-up комментирует...

Илья Каленик, между командами делается пауза при помощи Sleep(), которая нужна модему на обработку команды. Посмотрите в выложенном архиве с исходниками :)

tails-up комментирует...

razor, у меня подобных проблем не возникало, так же как и у коллег, которым этот код был передан для дальнейшей работы... я готова предположить, что нужно внимательно посмотреть состояние памяти и "прибирает" ли функция конвертации за собой :)

Анонимный комментирует...

Со склеенными сообщениями не разбирались случайно?

tails-up комментирует...

Нет, но в документах, ссылки на которые есть в посте, это детально описано, а в сети толпа примеров на этот счет :)

Unknown комментирует...
Этот комментарий был удален администратором блога.
Unknown комментирует...

Огромное спасибо за статью, а то поисковик, да и справочники предлагают отправлять русский текст исключительно через PDU, что похоже является ужасным геморроем.
Поскольку UCS-2 совпадает с нулевой страницей UTF-16, то можно упростить функцию перекодировки до следующего:

///
/// Преобразует текст из кодировки ОС по умолчанию (обычно CP1251) в Unicode (т.е. UTF-16)
///
/// Возвращает строковое представление шестнадцатиричных байтов, каждая пара которых развёрнута,
/// т.е. в формате пригодном для отправки в СМС
public static string ConvertDefaultToUCS2(string input) {
byte[] resultBytes = Encoding.Convert(Encoding.Default, Encoding.Unicode, Encoding.Default.GetBytes(input));
for (int i = 0; i < resultBytes.Length; i += 2) {
var byteA = resultBytes[i];
var byteB = resultBytes[i + 1];
resultBytes[i] = byteB;
resultBytes[i + 1] = byteA;
}
string result = BitConverter.ToString(resultBytes).Replace("-", "");
return result;
}

tails-up комментирует...

Mikhail Mayurov, спасибо за полезный комментарий. Дублирующиеся комментарии я удалила :) Тут премодерация работает.

Unknown комментирует...

Vielen Dank! =)

tails-up комментирует...

Оказывается, это до сих пор актуально? :)

Анонимный комментирует...

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

kabz комментирует...

Обновите пожалуйста ссылочку на архив - ссылка битая... Ну или на почту отправьте архив, если не сложно. Спасибо! =)

tails-up комментирует...

Надо найти исходники, как только раскопаю в архивах последнюю версию (в прошлом году для Формулы 1 в Сочи делали такое приложение для сотрудников сервисдеска, чтобы они могли отправлять ссылки и статусы по обращениям внешним пользователям и гостям мероприятия).

Suborg комментирует...

Безотносительно используемых технологий, надо констатировать важную вещь: PDU стал де-факто стандартом, а текстовый режим современными мобилами поддерживается кое-как. Например, новая Nokia 105 понимает AT+CSCS, но вообще и в принципе не понимает AT+CSMP, что означает, что в UCS-2 в текстовом режиме по факту шиш что отправишь, только латиницу.

tails-up комментирует...

Возможно, именно на телефонах это так, модемы текстовый режим понимают вполне, а изменений в спецификации GSM вроде не было, или я не заметила новой ревизии...
Да и статье уже 4 года. Я когда ее писала, вообще не рассчитывала на такую долгую ее жизнь :) Хотя еще в 2014 году мы вполне успешно применили этот же механизм в другом проекте - в рассылке оповещений пользователям Service Desk на Формуле 1 (на первом Гран-при России в 2014 году).

artzin комментирует...

Дальше это команды
_port.Write("AT+CMGF=0\r\n");
не идет.
"Время ожидания операции записи истекло."

artzin комментирует...

Дальше команды
_port.Write("AT+CMGF=0\r\n");
не идет.

Время ожидания операции записи истекло.

Romix комментирует...

Не получается скачать Ваш пример Form приложения. Выдает сообщение: Возможно, элемент удален, его срок действия истек или отсутствует разрешение на просмотр. Мне как новичку в программировании без примера сложно разобраться полностью. Если возможно перевыложите пожалуйста пример приложения в VS2012 вроде он у вас был. Заранее благодарен.

Unknown комментирует...

Спасибо за статью. Почерпнул полезного, но от себя добавлю.
Во-первых, SerialPort.GetPortNames возьмет данные из реестра и вывалит вам массив с именами всех когда-либо подключенных устройств, как активных в данных момент, так и отключенных. Перебирайте потом десяток портов. (Не верите? Потыкайте модем в разные USB порты.)
Мой вариант через WMI (взят из контекста кода):

using (var managementObjectSearcher = new ManagementObjectSearcher("SELECT * FROM Win32_PnPEntity WHERE Caption like '%(COM%'"))
{
string[] portNames = SerialPort.GetPortNames();
IEnumerable ports = managementObjectSearcher.Get().Cast().ToList().Select(p => p["Caption"].ToString());

Settings.ServiceComPort = portNames.FirstOrDefault(x => ports.Any(p => p.Contains(x) && p.Contains("3G PC UI Interface")));
Settings.AppComPort = portNames.FirstOrDefault(x => ports.Any(p => p.Contains(x) && p.Contains("3G Application Interface")));
}

Мне были нужны 2 порта: командный и дата-порт модема (он же application, для передачи звука).
"Settings" - класс с различными потокобезопасными членами, там же создал два свойства для портов.
В чем суть данного костыля, спросите вы? А я отвечу. Данным образом я на 100% определяю необходимые мне ИМЕННО АКТИВНЫЕ порты по их именам (имена см. в диспетчере устройств). Дело в том, что таблица Win32_PnPEntity содержит информацию только о подключенных устройствах.

Во-вторых, SerialPort имеет очень удобное свойство - NewLine. Устанавливаете MySerialPort.NewLine = Environment.NewLine; Используете MySerialPort.WriteLine("AT"); и забываете напрочь про добавление символов перевода строк в AT-командах. А .Write() используйте для передачи буфера в порт, когда надо слать кусками.