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 можно так:
- в первую очередь, стоит внимательно прочитать соответствующий раздел спецификации 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 // инициализация модема, в ответ должно прийти 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(); }
Собственно, и все. Особо внимательные могут найти небольшой “баг” в приведенной выше функции…
24 комментария:
Доброго времени суток. У меня возникли вопросы по теме. Отправляю смс в режиме pdu, проходит какое-то время и модем виснет, приходится вручную вытаскивать его из компа и подключать снова. Что может быть причиной? Подскажите, как добиться его стабильной работы? пробовал решить проблему извращенным способом: перезагружать приемник/передатчик после отправки каждой смс, результата не вышло, только появилась катастрофическая задержка между получением, обработкой и отправкой ответной смс, особенно когда их много. Мыслей по этому поводу уже нет. ВЗЫВАЮ И МОЛЮ О ПОМОЩИ.
Ну, вот, к сожалению принципиально чем-то помочь не смогу, наверное. Кроме того, что я бы попробовала поменять модем - может, это глюк конкретного модема. А вот если будет повторяться на других модемах - тогда сложнее. Тогда надо смотреть, что творится в регистрах модема непосредственно перед зависанием - логировать все, до чего дотянетесь...
И, кстати, как по мне - если ваши сообщение не длиннее 70 символов, то их можно отправлять в text mode на русском языке :)
Подскажите, для отправки смс, вы открываете порт и сразу отправляете команды для отправки (перевод в нужный режим, отправка смс и тд)? или что-то еще? просто я прочитал, что надо еще включать приемник/передатчик модема, вот думаю, может из-за этого модем галит...
Попробовал скомпилировать ваш код. Все отлично, спасибо за исчерпывающую информацию. Есть только одна проблема. Первая СМСка отлично отправляется, а на второй программа начинает кушать память ведрами, полностью грузить проц на 50% и зависает напрочь. И модем отваливается от системы. Где-то в районе конвертации в UCS2. Как думаете, в чем может быть проблема.
Илья Каленик, между командами делается пауза при помощи Sleep(), которая нужна модему на обработку команды. Посмотрите в выложенном архиве с исходниками :)
razor, у меня подобных проблем не возникало, так же как и у коллег, которым этот код был передан для дальнейшей работы... я готова предположить, что нужно внимательно посмотреть состояние памяти и "прибирает" ли функция конвертации за собой :)
Со склеенными сообщениями не разбирались случайно?
Нет, но в документах, ссылки на которые есть в посте, это детально описано, а в сети толпа примеров на этот счет :)
Огромное спасибо за статью, а то поисковик, да и справочники предлагают отправлять русский текст исключительно через 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;
}
Mikhail Mayurov, спасибо за полезный комментарий. Дублирующиеся комментарии я удалила :) Тут премодерация работает.
Vielen Dank! =)
Оказывается, это до сих пор актуально? :)
Тема интересная. Я со склеенными сообщениями разобрался, но при массовой отправке некоторые сообщения не доходили.
Было бы очень круто если бы статью дополнили.
Обновите пожалуйста ссылочку на архив - ссылка битая... Ну или на почту отправьте архив, если не сложно. Спасибо! =)
Надо найти исходники, как только раскопаю в архивах последнюю версию (в прошлом году для Формулы 1 в Сочи делали такое приложение для сотрудников сервисдеска, чтобы они могли отправлять ссылки и статусы по обращениям внешним пользователям и гостям мероприятия).
Безотносительно используемых технологий, надо констатировать важную вещь: PDU стал де-факто стандартом, а текстовый режим современными мобилами поддерживается кое-как. Например, новая Nokia 105 понимает AT+CSCS, но вообще и в принципе не понимает AT+CSMP, что означает, что в UCS-2 в текстовом режиме по факту шиш что отправишь, только латиницу.
Возможно, именно на телефонах это так, модемы текстовый режим понимают вполне, а изменений в спецификации GSM вроде не было, или я не заметила новой ревизии...
Да и статье уже 4 года. Я когда ее писала, вообще не рассчитывала на такую долгую ее жизнь :) Хотя еще в 2014 году мы вполне успешно применили этот же механизм в другом проекте - в рассылке оповещений пользователям Service Desk на Формуле 1 (на первом Гран-при России в 2014 году).
Дальше это команды
_port.Write("AT+CMGF=0\r\n");
не идет.
"Время ожидания операции записи истекло."
Дальше команды
_port.Write("AT+CMGF=0\r\n");
не идет.
Время ожидания операции записи истекло.
Не получается скачать Ваш пример Form приложения. Выдает сообщение: Возможно, элемент удален, его срок действия истек или отсутствует разрешение на просмотр. Мне как новичку в программировании без примера сложно разобраться полностью. Если возможно перевыложите пожалуйста пример приложения в VS2012 вроде он у вас был. Заранее благодарен.
Спасибо за статью. Почерпнул полезного, но от себя добавлю.
Во-первых, 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() используйте для передачи буфера в порт, когда надо слать кусками.
Отправить комментарий