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



21 комментарий:

Илья Каленик комментирует...
Этот комментарий был удален администратором блога.
Илья Каленик комментирует...

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

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

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

Илья Каленик комментирует...

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

Илья Каленик комментирует...
Этот комментарий был удален автором.
razor комментирует...

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

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

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

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

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

Ivan Komogortsev комментирует...

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

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

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

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

Огромное спасибо за статью, а то поисковик, да и справочники предлагают отправлять русский текст исключительно через 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, спасибо за полезный комментарий. Дублирующиеся комментарии я удалила :) Тут премодерация работает.

Sergey Obolenskiy комментирует...

Vielen Dank! =)

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

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

Ivan Komogortsev комментирует...

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

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

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

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

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

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

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

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

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

Alex Zloy комментирует...

Спасибо за статью. Почерпнул полезного, но от себя добавлю.
Во-первых, 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() используйте для передачи буфера в порт, когда надо слать кусками.