В один прекрасний момент в одному з проектів виникла необхідність програмно відправляти російські смс через GSM-модем за допомогою AT-команд. Серверна частина проекту реалізована на Java, тому приклад буде саме на цій мові. Для тих, кому цікаво - ласкаво просимо під кат.
Трохи вхідних даних: сервер з COM-портом (RS232), GSM-модем Cinterion (Siemens) MC52i. Модем працює з AT-командами.
Першим пунктом рішення поставленого завдання став пошук способу подружити Java з COM-портом. На щастя, пошук в Гуглі практично відразу видав дуже зручну бібліотеку java-simple-serial-connector і документацію по ній. Відповідно, качаємо бібліотеку, підключаємо її до проекту, створюємо клас, який буде працювати з модемом і пишемо в заголовку
import jssc.SerialPort;
import jssc.SerialPortEvent;
import jssc.SerialPortEventListener;
import jssc.SerialPortException;
Бібліотека підключена. Пишемо клас для роботи з модемом
public class SMSSender public SMSSender () <>
private static SerialPort serialPort;
public static boolean smsSend (String sms, String phone, JspWriter out) throws IOException
// Передаємо в конструктор ім'я порту
serialPort = new SerialPort ( "COM1");
try // Відкриваємо порт
serialPort.openPort ();
// Виставляємо параметри
serialPort.setParams (SerialPort.BAUDRATE_9600,
SerialPort.DATABITS_8,
SerialPort.STOPBITS_1,
SerialPort.PARITY_NONE);
// Якщо потрібно реагувати на відповіді модему - вішаємо Лістенер і Парс відповіді. Я це опущу, кому потрібно-в документації є
//serialPort.addEventListener(new PortReader (), SerialPort.MASK_RXCHAR);
return true;
>
catch (SerialPortException ex) out.println (ex);
return false;
>
Так ми відкрили і тут же закрили з'єднання з когось портом. Тепер безпосередньо як послати російську смс. Щоб це зробити, з модемом потрібно спілкуватися в PDU-форматі. Відповідно, спочатку модем налаштовуємо на роботу в цьому режимі:
Далі повинна йти команда
AT + CMGS =<тело смс><Символ CTRL+Z>
Тепер як скласти це саме тіло смс в PDU-форматі. При пошуках відповіді я напоровся на наступні вихідні, які пояснюють заголовок (Код на Delphi, але там все зрозуміло)
Порядок проходження та значення байт заголовка зрозумілі. Тепер перейдемо як же закодувати текст повідомлення. Його потрібно передавати модему в UCS2-кодуванні. По суті це 7-ми бітна інверсна кодування. Функція конвертації в UCS2-формат на JAVA:
private static String StringToUSC2 (String text) throws IOException String str = "";
byte [] msgb = text.getBytes ( "UTF-16");
String msgPacked = "";
for (int i = 2; i
msgPacked + = b;
>
String msglenPacked = Integer.toHexString (msgPacked.length () / 2);
if (msglenPacked.length () <2) str += "0";
str + = msglenPacked;
str + = msgPacked;
Ну і далі наведу приклад свого класу для відправки російських смс-ок з поясненнями:
import jssc.SerialPort;
import jssc.SerialPortEvent;
import jssc.SerialPortEventListener;
import jssc.SerialPortException;
// Клас для відправки смс-повідомлень
public class SMSSender public SMSSender () <>
private static SerialPort serialPort;
// Функція конвертації тексту СМС-ки в USC2 формат разом з довжиною повідомлення (Значення, що повертається <длина пакета><пакет>)
private static String StringToUSC2 (String text) throws IOException String str = "";
byte [] msgb = text.getBytes ( "UTF-16");
// Конвертація самої СМС
String msgPacked = "";
for (int i = 2; i
msgPacked + = b;
>
// Довжина отриманого пакета в потрібному форматі
String msglenPacked = Integer.toHexString (msgPacked.length () / 2);
// Якщо довжина непарна - додаємо в кінці 0
if (msglenPacked.length () <2) str += "0";
// Формуємо рядок з довжини і самого тіла пакета
str + = msglenPacked;
str + = msgPacked;
// Отримати довжину повідомлення
private static int getSMSLength (String sms) return (sms.length () / 2 - 1);
>
public static boolean smsSend (String sms, String phone, JspWriter out) throws IOException
// Передаємо в конструктор ім'я порту
serialPort = new SerialPort ( "COM1");
try // Відкриваємо порт
serialPort.openPort ();
// Виставляємо параметри
serialPort.setParams (SerialPort.BAUDRATE_9600,
SerialPort.DATABITS_8,
SerialPort.STOPBITS_1,
SerialPort.PARITY_NONE);
//serialPort.addEventListener(new PortReader (), SerialPort.MASK_RXCHAR);
// Формуємо повідомлення
String message = "0011000B91" + reversePhone (phone) + "0008A7" + StringToUSC2 (sms);
// Відправляємо запит пристрою
char c = 0x0D; // Символ перекладу каретки CR
String str = "AT + CMGF = 0" + c;
serialPort.writeString (str);
Thread.sleep (500); // По-ідеї, тут треба чекати відповідь модему, але ми обмежимося просто очікуванням в півсекунди
// Очистимо порт
serialPort.purgePort (serialPort.PURGE_RXCLEAR | serialPort.PURGE_TXCLEAR);
//out.println(str);
str = "AT + CMGS =" + getSMSLength (message) + c;
serialPort.writeString (str);
Thread.sleep (500);
serialPort.purgePort (serialPort.PURGE_RXCLEAR | serialPort.PURGE_TXCLEAR);
//out.println(str);
c = 26; // Символ CTRL + Z
serialPort.writeString (message + c);
Thread.sleep (1000);
return true;
>
catch (SerialPortException ex) out.println (ex);
return false;
> Catch (InterruptedException e) //out.println(e);
return false;
>
// Клас зчитування відповідей. Я вирішив обійтися без нього, але в документації до JSSC все є 🙂
/ * Private static class PortReader implements SerialPortEventListener
public void serialEvent (SerialPortEvent event) if (event.isRXCHAR () event.getEventValue ()> 0) try // Отримуємо відповідь від пристрою, обробляємо дані і т.д.
String data = serialPort.readString (event.getEventValue ());
// І знову відправляємо запит
System.out.println ( "response:" + data);
>
catch (SerialPortException ex) System.out.println (ex);
>
>
>
> * /
>
P.S>
Теж використовував режим PDU. Метод reversPhone () можна уявити 4-я рядками
String phonenumPDU = «»;
phonenum = phonenum + »F»;
for (int i = 1; i<12;i=i+2) phonenumPDU = phonenumPDU + phonenum.charAt(i+1) + phonenum.charAt(i);
>
Згоден 🙂 Така реалізація більш правильна, але я коли писав - був азарт розібратися саме з модемом, тому реалізував даний метод «влобовую».