Введение

Over The Air Provision (OTAP)  TC65Т

GSM модем SL116 на базе модуля Enfora GSM0308

Пишите автору сайта ganatol2000@mail.ru

GSM модем Siemens TC65

Java программа измерения аналоговых сигналов и передачи их величины UDP сообщениями .

 

 Бросим наш ищущий взор на DEVICE от лидера промышленной автоматизации Siemens - модем TC65Т,
пусть даже, если сейчас он выпускается под маркой
   http://www.cinterion.com/tc65t.html
У этого модема имеются десять дискретных входов-выходов и два аналоговых входа, которые могут оцифровывать входное напряжение от 0 до 5 В. Но уникальность этого модема заключается в поддержке JAVA 2 ME. Это именно та платформа JAVA, которая используется для программирования всех мобильных телефонов. Единственное т. к. в TC65T не предусмотрен экран, то здесь JAVA 2 ME применяется с профилем IMP-NG. В общем имея элементарные навыки программирования этот модем можно заставить творить чудеса. Естественно он годиться и для нашего, весьма в общем не сложного случая: необходимо на аналоговые входы подавать сигналы с датчиков (например с датчика давления и расхода) и передавать по GPRS  на сервер информацию о величинах этих сигналов.
Как установить и настраивать софт с диска
TC65 JAVA SDK написано здесь http://gsmpager.ru/doc/tc65setup-HOWTO.txt. Сам диск можно скачать от сюда
TC65_SDK.rar    http://yadi.sk/d/vUdySCd82gSu9
Мне же необходимо заметить, что для отладки программы  модема нужно настроить программу
 HyperTerminal (Входящую в состав Windows) выставив параметры Com порта компьютера:

 

 

 

 

 

 

 

 

 

 

  Нужно сказать, что модем TC65 по умолчанию настроен таким образом, что стандартный вывод из программ JAVA у него происходит в COM порт ASC1. А RS-232, расположенный на корпусе модема, к которому мы подключим кабель от компьютера, есть COM порт ASC0. Значит мы установили весь софт включая Eclipse и создали нашу первую программу, использующую System.out.println("Hello world");

Потом  откомпилировали проект: левой кнопкой мыши Project - Build Project(или Build All), далее скомпоновали пакет: правой кнопкой мыши Hello world -  левой кнопкой мыши J2ME - Create Package, и из каталога deployed проекта загрузили образовавшиеся файлы helloworld.jar и helloworld.jad через "Проводник" в модем. Затем  попробовали запустить нашу программу: AT^SJRA=a:/helloworld.jar , а никакого Hello world мы не видим... Поэтому, чтобы все-таки пронаблюдать столь вожделенную надпись, необходимо обратить внимание на AT команду AT^SCFG. Советую вообще не пожалеть времени для изучения возможностей этой команды, а в нашем случае в HyperTerminal-e следует набрать AT^SCFG="Userware/Stdout","ASC0". И уже потом все будет видно...

Обращаю внимание, что команда
AT&F, устанавливающая заводские настройки, параметр "Userware/Stdout" не изменяет.

Дальше можно создавать нечто более серьезное
: будем измерять величину сигнала на аналоговых входах и передавать эту информацию через GPRS используя UDP протокол.
 Согласно документации один источник аналогового сигнала подключим на 21 и 22 контакты модема, а второй источник подключим на 9 и 10 контакты модема.




Величина аналогового сигнала подаваемого на выводы модема должна составлять 0...5 В. Как следует из рисунка на сам модуль подается преобразованный сигнал уровнем 0...2,4В.

Чтобы получить информацию о величине сигнала на  аналоговых входах будем использовать команды: AT^SRADC=0 с первого и AT^SRADC=1 со второго аналогового входа соответственно.

  В ответе модема фигурирует именно преобразованная величина аналогового сигнала.
А сколько же реально мы подавали на аналоговый вход
? Это придется вычислять используя параметры запрашиваемые командой AT^SAADC? .

Зная, что величина аналогового сигнала измеренного модемом составляет 876 мВ, то реальное значение сигнала поданного например на 2-ой аналоговый вход вычисляется следующим образом:
Value=(876-2)*8000/4096=1707 мВ
Но эти вычисления в рассматриваемой программе модема предусмотрены не будут.
 Сформулируем задание на разработку программы
:
Модем периодически измеряет напряжение на своих аналоговых входах и отсылает эту информацию в виде UDP-сообщений на сервер.
 1. Передаваемая датаграмма состоит из:
     short Id - уникальный номер(идентификатор)  модема
    
int AI0 - значение аналогового сигнала подаваемого на первый аналоговый вход модема
    
int AI1 - значение аналогового сигнала подаваемого на второй аналоговый вход модема
    
int TimeModem - несколько модифицированное время модема в формате UNIX.
(
UNIXвремя - время в секундах, момент отсчета считается полночь с 31 декабря 1969 года на 1 января 1970 года, время с этого момента называют "эрой UNIX" ) т. к.  10 января 2004 годы в 13:37:04 UNIX время достигло значения 1073741824, то для экономии трафика есть смысл передавать значение UNIX времени уменьшенное на 1000000000.
2. Чтобы дать возможность серверу принимать информацию от нескольких модемов программа должна иметь возможность задавать уникальный идентификатор модема отличный от принятого по умолчанию. Для этого можно использовать 
Windows - приложение HyperTerminal.
3. Для синхронизации по времени работы модема и сервера и для обеспечения надежности доставки датаграммы от модема к серверу обеспечить прием модемом от сервера ответной датаграммы подтверждающей доставку информации. Если ответной датаграммы модемом не получено, то модем повторяет отправку своего
UDP-сообщения до 4-х раз.
Принимаемая модемом датаграмма состоит из
:
short idModem - Уникальный номер(идентификатор)  модема, тот же, что и принят сервером.
short RFlag - флаги позволяющие изменять характер датаграммы сервера.
RFlag.ask = 0x01 - подтверждение успешного приема
RFlag.time = 0x02 - текущее время
int timeR - Если установлен RFlag.time, то передается текущее время в формате UNIX.
Таким образом сервер, приняв UDP-сообщение от модема сравнивает время переданное модемом со своим текущим временем. Если разница превышает какую-то критическую величину, то в ответной датаграмме присутствуют как флаг подтверждения успешного приема так и флаг текущего времени (RFlag=0x03), а это означает, что в датаграмме есть еще поле текущего времени и модем должен установить это время у себя как текущее. Если же разница времен незначительна, то в датаграмме присутствует лишь флаг успешного приема (RFlag=0x01) и модем приняв такую дейтаграмму лишь прекращает повторную отправку своих UDP-сообщений до следующего цикла измерения.


Теперь приступим к разбору текста самой программы.
Класс
ProbaTC65new - основной класс приложения.
import javax.microedition.midlet.*;
import com.siemens.icm.io.*;


/* Основной класс приложения должен быть объявлен открытым (public)
 и должен дополнять javax.microedition.midlet.MIDlet.  Все MID-леты должны дополнять этот класс. */
public class ProbaTC65new extends MIDlet {
// Объявляем классы
ATCommand atCommand;
/* Класс ATCommand входит в состав com.siemens.icm.io.ATCommand. Служит для использования AT команд программах JAVA  */
Tc65SerialPort tc65SerialPort;
/*Методы класса Tc65SerialPort используются для организации ввода-вывода данных из Com-порта ASC0. Они будут работать даже если стандартный ввод-вывод  будет настроен на ASC1  */.
MenuSettings menuSettings;
/* Класс MenuSetting создает поток (Thread) в котором будет контролироваться  информация поступающая через Com -порт (нажатие клавиши компьютерной клавиатуры) и в зависимости от поданной команды будут                         выполняться необходимые действия. */
/* Конструктор основного класса должен быть объявлен открытым и безаргументным. */
public ProbaTC65new() {

try
{
/* Создаем объявленные классы */
tc65SerialPort = new Tc65SerialPort();
atCommand = new ATCommand(false);
menuSettings = new MenuSettings(tc65SerialPort, atCommand);
/* Запускаем поток класса MenuSettings*/
menuSettings.start();

}


catch(Exception e)
{
System.out.println(e);
notifyDestroyed();
}
}


void serialOut(String str)
{
tc65SerialPort.serialOut(str.getBytes());
}
void serialOut(byte[] ar)
{
tc65SerialPort.serialOut(ar);
}


/*  Главный метод. Точка входа в программу */
public void startApp() throws MIDletStateChangeException {
// System.out.println("Begin\n\r");
init();
/* Цикл с условием пока поток menuSettings существует */
while(menuSettings.isAlive())
{
try {
Thread.sleep(150);
} catch (Exception ioe) {
serialOut("Excp "+ioe.getMessage()+">");
}
}

// serialOut("\n\rJava app closed");
destroyApp(true);
}


/* Функция pauseApp определена структурой MID-лета. Вызывается когда приложение переходит в режим паузы*/
public void pauseApp() {
System.out.println("\npauseApp()\n");
}


/* Функция destroyApp определена структурой MID-лета. Вызывается при закрытии приложения*/
public void destroyApp(boolean unconditional) {
System.out.println("\n\rdestroyApp(" + unconditional + ")\n");
notifyDestroyed();
}

/* Функция производит начальную инициализацию модема: установку заводских настроек, ввод PIN -кода*/
void init()
{
try
{
atCommand.send("AT&F\r"); // Сброс
Thread.sleep(1000);
// чтобы модем успел прочухаться
atCommand.send("AT+CPIN=0000\r");
Thread.sleep(5000);
// чтобы модем успел прочухаться
}
catch(ATCommandFailedException e)
{
// serialOut("ERROR: init failed\n\r");
}
catch (InterruptedException e)
{
// serialOut("ERROR: init interrupted\n\r");
}
}

}
 

/* Класс MenuSettings */
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import javax.microedition.io.Connector;
import java.lang.String;
import com.siemens.icm.io.*;
import com.siemens.icm.io.file.FileConnection;
/*Объявлен как расширение класса Thread, значит в нем будет выполняемый метод run()*/
public class MenuSettings extends Thread {
/* Объявляем классы */
Tc65SerialPort tc65SerialPort;
ATCommand atCommand;
SendMessageUDP aI;
/* Класс SendMessageUDP  создает поток, который получает информацию  о величине аналоговых сигналов, дате и времени, и затем  формирует и передает UDP-дейтаграмму */
time t;
/*Класс time функции которого изменяют и считывают системное время модема*/
public String idNumberPhone;
/* Переменная используемая для хранения идентификатора модема*/
StringBuffer strBuf;
/* Буфер памяти куда считывается символы набранные на клавиатуре при изменении идентификационная информация модема*/
boolean FillingBuffer = false;
/* Переменная принимающая значение true если пользователь принял решение изменить идентификатор модема  */
int idxInputBuffer=0;
/* Переменная, которая содержит информацию о количестве введенных символов при изменении  идентификатора модема   */
int NowFillingValue = 0;
/* Если значение этой переменной становиться равным 1, то заканчивается ввод нового идентификатора модема  и в idNumberPhone считывается информация из strBuf*/
boolean aiStart=false;
  /* Переменная  с помощью которой запускается или останавливается поток класса SendMessageUDP и происходит или прекращается передача UDP-датаграмм*/


// Константы для работы с меню
public static final char MS_NODATA = 0;
// Передавать в ProcessMenu, когда нет данных.

// Состояние ProcessMenu()
public static final int MS_HAVEAITERM=4;
/* Если  ProcessMenu()  возвращает это значение, то процесс измерения аналогового сигнала  и формирования сообщений заканчивается*/
public static final int MS_HAVE_AI_GPRS= 5;
 
/* Если ProcessMenu()  возвращает это значение, то запускается процесс измерения аналогового сигнала, с передачей результатов измерения в иде UDP-датаграмм*/
public static final int MS_EXITPROG = 1;
/* Если  ProcessMenu() возвращает это значение то выполнение программы прекращается */
public static final int MS_DONTEXIT = 0;
/* Выполнение каких либо действий не требуется, но программа должна работать */

int CurrMenu = 0;
// Текущий пункт меню (0 - нет меню)
boolean DefaultStart;
/* Переменная которая при запуске программы имеет значение false и если не происходит нажатие клавиши на клавиатуре то запускается процесс передачи UDP-датаграмм без дополнительной команды. Если же произошло нажатие клавиши на клавиатуре, то флаг принимает значение true и дальнейшее управление                               программой происходит при помощи меню */

/* Конструктор MenuSettings*/
public MenuSettings(Tc65SerialPort tc65SerialPort, ATCommand atCommand)
{

/* Эти классы были созданы в основном классе приложения ProbaTC65 сюда же переданы в качестве параметров, поэтому здесь мы их не создаем, а получаем на них указатель*/
this.tc65SerialPort = tc65SerialPort;
this.atCommand = atCommand;
/*При запуске программы идентификатору присваиваем значение по умолчанию. Глобальные константы хранятся в классе Const*/
idNumberPhone=String.valueOf(Const.Id_TC65AnalogMessage);
/* Создаем буфер памяти куда считываются символы набранные на клавиатуре при изменении идентификационная информация модема*/
strBuf = new StringBuffer(
6);
/*Если при запуске программы не произойдет нажатие клавиши на клавиатуре, то запуститься процесс передачи UDP-датаграмм*/
DefaultStart=false;
/*Создаем класс для работы с системным временем*/
t=new time();
/* Пытаемся загрузить идентификатор модема из файла, если не получилось,
то создаем файл с идентификатором по-умолчанию
*/
if(LoadFromFile()==false)
{
SaveToFile(idNumberPhone);
try {
sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}

}
/*Эта функция выводит в com-порт строку сообщения о том какой же идентификатор присвоен модему*/
idSettings();
}

/*Метод run(), у класса объявленного как поток(Thread) должен быть определен этот метод, там содержится тело потока. Когда вызывается метод  start()  (menuSettings.start(); в классе ProbaTC65new) интерпретатор создает новый поток для выполнения метода run(). Этот новый поток продолжает выполняться пока метод run() не закончит работу, после чего поток перестает существовать.  */
public void run()
{
try
{
do{
sleep(10000);
int ch;
/* Тут мы проверяем наличие данных, поступающих из Com-порта и если они имеются, то запускаем функцию ProcessMenu*/
if(tc65SerialPort.hasData() > 0)
{
DefaultStart=true;
if(aiStart==true)
{
aiStart=false;
aI.puskAI(false);
}

ch = tc65SerialPort.readByte();
if (ch >= 0)
{
int retcode = ProcessMenu((char)ch);
/*В зависимости от того какое значение вернула функция ProcessMenu выполняем нужные действия*/
/* Закрываем JAVA приложение*/
if(retcode == MS_EXITPROG)
{
close();
break;
}
/*Создаем поток SendMessageUDP в котором происходит передача информации о величине аналоговых сигналов в UDP-датаграмме*/
else if(retcode==MS_HAVE_AI_GPRS)
{
if(aiStart==false)
{
tc65SerialPort.serialOut("\nStart analog value gprs\n\r");
/*Создаем класс SendMessageUDP*/
aI=new SendMessageUDP(tc65SerialPort, atCommand, t, idNumberPhone);
aI.puskAI(true);
/*Запускаем поток SendMessageUDP*/
aI.start();
aiStart=true;
}

}

/* Закрываем поток созданный SendMessageUDP */
else if(retcode==MS_HAVEAITERM)
{
if(aiStart==true)
{
aiStart=false;
aI.puskAI(false);
}

}

}

}

/* Если при старте программы модем не был подключен к com-порту компьютера с запущенным windows-приложением HyperTerminal или, если даже он и был подключен но не была нажата ни одна клавиша на клавиатуре, это означает, что поступление данных из com-порта не происходит, то сразу запускается поток SendMessageUDP в катором осуществляется передача информации о величине сигналов на аналоговых входах модема в UDP-датаграмме*/  
else
{
if(DefaultStart==false)
{
tc65SerialPort.serialOut("\nStart analog value gprs\n\r");
/*Создаем класс SendMessageUDP*/
aI=new SendMessageUDP(tc65SerialPort, atCommand, t, idNumberPhone);
aI.puskAI(true);
/*Запускаем поток SendMessageUDP*/
aI.start();
aiStart=true;
DefaultStart=true;
}

}

}while(true);

}
catch (InterruptedException e)
{
tc65SerialPort.serialOut("InterruptedException, closing MenuSettings thread");
close();
}

}

/* В качестве параметра в функции  ProcessMenu используется код символа поступающего из Com-порта*/
int ProcessMenu(char NextEntered)
{
boolean StopProcessing = false;
// Можно сделать false, чтобы войти еще раз.
int retval = MS_DONTEXIT;
// что вернуть

while(StopProcessing == false)
{
StopProcessing = true;
switch(CurrMenu)
{
case 0:
// Не было меню CurrMenu=0
if(NextEntered == 0x0d)
// Нажали Enter
{
CurrMenu = 1;
// Переходим в меню 1 и обрабатываем нажатие Enter
StopProcessing = false;
}
break;

case 1:
// Мы в основном меню CurrMenu=1
switch(NextEntered)
{
/*В Сom-порт посылаем текст, который в окне HyperTerminal будет виден как  основное меню. */
case 0x0d:
// Enter
retval=MS_HAVEAITERM;
if(aiStart==false)
{
tc65SerialPort.serialOut( "TC65 menu\n\r" +
"-----------------\n\r" +
"2 - Analog gprs\n\r" +
"4 - Enter id modem\n\r" +
"9 - Exit Java APPLICATION\n\r");
}
break;

/* Нажатием клавиши '2' дается команда  на создание потока SendMessageUDP
c передачей информации о величине сигналов на аналоговых входах модема в UDP-датаграмме*/
case '2':
//
NextEntered=2
{
retval=MS_HAVE_AI_GPRS;
}
break;

/* Нажатие клавиши '4' приводит к появлению меню ввода нового идентификационного номера модема (CurrMenu = 2) */
case '4':
//
NextEntered=4
CurrMenu = 2;
StopProcessing = false;
// показать меню след. уровня
NextEntered = 0x0d;
// типа нажали ENTER
break;

/*Нажатием клавиши '9' дается команда на завершение JAVA программы выполняемой модемом*/
case '9':
//NextEntered=9
retval= MS_EXITPROG;
break;

/*По-умолчанию, если  код нажатой клавиши поступивший из Com-порта не предусмотрен основным меню, то посылается команда на завершение измерения аналоговых сигналов и прекращение передачи UDP-датаграмм, а в Com-порт (в окно HyperTerminal) посылается текстовое сообщение, требующее нажать Enter для вывода основного меню*/
default:
retval=MS_HAVEAITERM;
//Посылаем команду на завершение потока SendMessageUDP
if(aiStart==false)
{
tc65SerialPort.serialOut("Press Enter to display menu.\r\n");
}
break;
}
break;
//
CurrMenu=1

/*Меню ввода нового идентификатора модема*/
case 2:
// При нахождении в главном меню была нажита клавиша '4' и CurrMenu  приняло значение '2' (CurrMenu=2)
/ * Когда FillingBuffer=true, то уже происходит ввод нового идентификатора модема и каждый код поступающий из Com-порта (NextEntered)  есть символ этой информации*/
if(FillingBuffer==true)
{
// Если не служебный символ
if(NextEntered > ' ')
{
if(idxInputBuffer <
5) // размер меньше длины буфера на 1
{
/*Память под strBuf была выделена в конструкторе класса и сейчас туда добавляется каждый вновь введенный символ,*/
strBuf.append(NextEntered);
idxInputBuffer++;
tc65SerialPort.serialOut(NextEntered);
}
else
// буфер сейчас переполнится, заканчиваем ввод
{
FillingBuffer = false;
NextEntered = 0x0d;
// Чтобы показать меню
}
}
else
// Служебный символ или ENTER
{
FillingBuffer = false;
NextEntered = 0x0d;
// Чтобы показать меню

tc65SerialPort.serialOut("\n\rDEBUG: Entered " + strBuf.toString() + "\n\r");
// Запомним и покажем то, что ввели
switch(NowFillingValue)
{
case 1:
// Номер
if(idxInputBuffer > 0)  idNumberPhone = strBuf.toString();
tc65SerialPort.serialOut("\n\rNew id modem: " + idNumberPhone + "\n\r");
break;
}
}
}
//if(FillingBuffer)


/
*Когда FillingBuffer=false, то в Com-порт посылается информация, которая в окне HyperTerminal будет видна как меню ввода идентификатора модема*/
else if(FillingBuffer == false)
{
idxInputBuffer = 0;
// Где-то надо обнулять индекс...
switch(NextEntered)
{
case 0x0d:
tc65SerialPort.serialOut( "\n\rChange settings menu\n\r" +
"-----------------\n\r" +
"1 - Change id modem\n\r" +
"8 - Save settings to file\n\r" +
"0 - Exit menu\n\r\n\r");
break;

/*При нажатии клавиши '1' в Com-порт поступает информация о текущем идентификаторе модема, FillingBuffer присваивается значение true и программа переходит к обработке каждого вновь введенного символа как символа нового идентификатора */
case '1':
NowFillingValue = 1;
FillingBuffer = true;
tc65SerialPort.serialOut("\n\rChange id modem\n\r" +
"-----------------------\n\r" +
"Current id modem:" + idNumberPhone +
"\n\rEnter new id modem:\n\r");
break;

/*При нажатии клавиши '8' происходит сохранение идентификатора модема в файл,
чтобы при перезагрузке он не был утерян и считывался из файла
*/
case '8':
tc65SerialPort.serialOut("\n\rSave settings to file\n\r");
try {
sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
SaveToFile(idNumberPhone);
break;

/*При нажатие клавиши '0' CurrMenu присваивается значение 1 и программа переходит к отображению основного меню*/
case '0':
CurrMenu = 1;
// Возврат на уровень выше
NextEntered = 0x0d;
StopProcessing = false;
break;

default:
tc65SerialPort.serialOut("Unknown command! Press Enter to display menu.\r\n\n\r");
break;
}

// Будем заполнять буфер заново?
if(FillingBuffer == true)
strBuf.delete(0, strBuf.length());
}
// else if(FillingBuffer)
break;

// ******************* default ******************
default:
// В случае глюков
tc65SerialPort.serialOut("ERROR! Bad CurrMenu value.\n\r");
CurrMenu = 0;
NextEntered = MS_NODATA;
break;

}
}
return retval;
}


//Функция вызывается при закрытии процесса MenuSettings
public void close()
{
// выводим здесь, потому что потом закроем СОМ-порт -
// и выводить будет некуда.
tc65SerialPort.serialOut("\n\rTC65 test Java app application finished\n\r");
System.out.println("Streams and connection closed");
}

/*Функция считывает из файла идентификатор модема и присваивает его значение переменной idNumberPhone, если успешно возвращает true */
boolean LoadFromFile()
{
boolean my_ret=false;

try
{
/*Открываем файл на чтение*/
FileConnection fconn = (FileConnection)Connector.open("file:///a:/"+Const.FileName,
Connector.READ);

if (fconn.exists())
{
long size = fconn.fileSize();

if(size <6)
// short FFFF=65535 5-символов
{
DataInputStream dis = fconn.openDataInputStream();
//открываем поток данных из файла
idNumberPhone = dis.readUTF();
dis.close();
//закрываем поток данных из файла
my_ret=true;

}
else
{
tc65SerialPort.serialOut("Probably wrong contents of settings file; using defaults\n\r");
my_ret=false;
}
}
else
{
tc65SerialPort.serialOut("Settings file not found; using defaults\n\r");
my_ret=false;
}
fconn.close();
/*Закрываем файл*/

}
catch (IOException ioe)
{
tc65SerialPort.serialOut("File read operation error: " + ioe.getMessage() + "\n\r");
my_ret=false;
}
return my_ret;
}

/*Функция сохраняет в файл идентификатор модема (NumberPhoneSave) переданный в качестве параметра*/
public void SaveToFile(String NumberPhoneSave)
{
try
{
/*Открываем файл*/
FileConnection fconn = (FileConnection)Connector.open("file:///a:/"+Const.FileName);


//Если файл не существует он будет создан
if (!fconn.exists())
{
fconn.create();
// create the file if it doesn't exist
DataOutputStream os = fconn.openDataOutputStream();
//открываем поток данных в файл
try{
os.writeUTF(NumberPhoneSave);
tc65SerialPort.serialOut("\n\rThe "+Const.FileName+" file has been saved to module file system\n\r");
}finally {os.close();
//закрываем поток данных в файл}
}
//Если файл существует он будет перезаписан
else if (fconn.exists())
{
fconn.truncate(0);
DataOutputStream dos = fconn.openDataOutputStream();
//открываем поток данных в файл
try{
dos.writeUTF(NumberPhoneSave);
tc65SerialPort.serialOut("\n\rThe "+Const.FileName+" file has been saved to module file system\n\r");

}finally {dos.close();
//закрываем поток данных в файл}
}
else
tc65SerialPort.serialOut("Could not create output file, settings not stored\n\r");

fconn.close();
/*Закрываем файл*/
}
catch (IOException ioe)
{
tc65SerialPort.serialOut("File write operation error: " + ioe.getMessage() + "\n\r");
}
}


//При запуске процесса MenuSettings выводиться текстовое сообщение о текущем идентификаторе модема
void idSettings()
{
tc65SerialPort.serialOut( "\n\rTC65 id settings =" + idNumberPhone + "\n\r\n\r");
}

}
// End Class MenuSettings

/*Класс Tc65SerialPort используются для организации ввода-вывода данных из Com-порта ASC0  */
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.microedition.io.CommConnection;
import javax.microedition.io.Connector;

public class Tc65SerialPort
{
CommConnection commConn;
InputStream inStream;
OutputStream outStream;
/* Конструктор задает параметры Com-порта ASC0 и создает входной и выходной потоки,
которые затем будут использоваться в функциях для работы с данными
*/
public Tc65SerialPort()
{
try
{
String strCOM = "comm:com0;blocking=on;baudrate=115200";
commConn = (CommConnection)Connector.open(strCOM);
inStream = commConn.openInputStream();
outStream = commConn.openOutputStream();
}
catch (IOException e)
{
e.printStackTrace();
}
}

/**
* Вывод информации в СОМ-порт
* @param s - String
*/
public void serialOut(String s)
{
serialOut(s.getBytes());
}
/**
* Вывод информации в СОМ-порт
* @param b - byte[] array
*/
public void serialOut(byte[] b)
{
try
{
outStream.write(b);
}
catch(IOException e)
{
System.out.println(e);
}
}

/**
* Вывод информации в СОМ-порт
* @param с - char
*/
public void serialOut(char c)
{
try
{
outStream.write(c);
}
catch(IOException e)
{
System.out.println(e);
}
}

/* Проверка наличия данных поступающих через Com-порт от компьютера в модем */
public int hasData()
{
int n = 0;
try
{
n = inStream.available();
}
catch (IOException e)
{
}
return n;
}

/* Чтение данных поступающих через Com-порт*/
int readByte()
{
int NextByte = 0;
try {
NextByte = inStream.read();
} catch (IOException e) {
e.printStackTrace();
}
return NextByte;
}
}
// End Class  Tc65SerialPort


/*Класс AnswerAnalog использует метод "ATResponse" определенный в интерфейсе
 ATCommandResponseListener. Этот класс обрабатывает ответ модема на AT-команду AT^SRADC  которая производит запрос величины сигнала на аналоговом входе */
import com.siemens.icm.io.*;
public class AnswerAnalog implements ATCommandResponseListener {

public String Message="";
public int value;
/* Конструктор хоть пустой, но должен быть*/
public AnswerAnalog()
{

}

public void ATResponse(String resp) {

try {
/* Разберем ответ модема: ^SRADC: 1,1,876
1 - ответ о результатах измерения со 2-го аналогового входа
1 - аналоговый вход открыт (думаю, что если бы он был закрыт, то и измерения бы не было)
876 - а это величина аналогового сигнала в мВ. Таким образом в строке ответа модема символы показывающие величину аналогового сигнала начинаются с 25 и заканчиваются за 8 символов до конца строки (ОК нам ведь не нужен).
*/
int a=resp.length();
String analog=resp.substring(25,a-8).trim();
value = Integer.parseInt(analog,10);

Message=Integer.toString(value);

} catch (NumberFormatException e) {
e.printStackTrace();
}
}

}
// End Class  AnswerAnalog
 

/*Класс  AnswerTime также использует метод "ATResponse" из интерфейса ATCommandResponseListener. Он обрабатывает ответ модема на AT-команду AT+CCLK?, которая запрашивает  дату и время модема */
import com.siemens.icm.io.*;
public class AnswerTime implements ATCommandResponseListener {
public String Message="";

/* Конструктор хоть пустой, но должен быть*/
public AnswerTime()
{

/*Эта функция определена в интерфейсе ATCommandResponseListener, т. к. мы используем класс AnswerTime для  получения даты и времени модема, то должны разобрать ответ модема на команду AT+CCLK? и из этой текстовой информации выбрать ту, что непосредственно отображает дату и время*/

public void ATResponse(String resp) {
try {
String time=resp.substring(19,36);
Message=time;
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
}
// End Class  AnswerTime

/*Класс time требует более детального пояснения. Дело в том, что для отображения текущего времени модема на экране компьютера мы используем команду AT+CCLK?  , а в UDP-датаграмме используется время в формате UNIX (в миллисекундах) получаемое с помощью функции System.currentTimeMillis(), но в процессе отладки программы выяснилось, что актуальное время установленное  AT+CCLK="ДД/ММ/ГГ,чч:мм:сс" не соответствует времени считанному функцией System.currentTimeMillis() пока программа модема не будет перезапущена. Это можно было бы сделать используя команды: будильника AT+CALA="ДД/ММ/ГГ,чч:мм:сс" и выключения модема AT^SMSO. Но подобное пришлось бы делать каждый раз, когда сервер решит подкорректировать время. По-этому просто была введена переменная delta в которой учитывается разница между временем полученным с сервера и временем считанным System.currentTimeMillis(). В дальнейшем эта переменная используется для вычисления актуального времени:
delta+
System.currentTimeMillis()*/

public class time {
/*UNIX время в 2004 году стало больше значения 1000000000000L в миллисекундах, поэтому эту цифру можно вычесть из времени считанного функцией  System.currentTimeMillis()*/
protected final static long timeOffset = 1000000000000L;
/*Время считанное System.currentTimeMillis() измеряется в миллисекундах, а нам нужно время в секундах поэтому время считанное System.currentTimeMillis() делится на 1000L*/
protected final static long timeCoeff = 1000L;
/*Первоначально переменной delta присваивается 0*/
private int delta = 0;
/*Этот массив используется для вычисления месяца из числа обозначающего UNIX временя*/
private final static byte dim[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
/*Из числа обозначающего UNIX временя мы должны вычислить секунду, минуту, час, день, месяц, год. И затем эти значения будут использоваться в команде AT+CCLK="ДД/ММ/ГГ,чч:мм:сс" для установки актуального времени*/
private short ms, sec, min, hour, day, mon, year;
/*Функции получения актуального времени в UNIX ajhvfnt с учетом переменной delta и коэффициентов timeOffset и timeCoeff*/
public static int long2int(long t) {return new Double((t - timeOffset) / timeCoeff).intValue();}
public static int intTime() {return long2int(System.currentTimeMillis());}
public static long int2long(int t) {return t * timeCoeff + timeOffset;}
public int getTime() {return time.intTime() + delta;}

/* Конструктор класса time*/
public time() {setNewTime(System.currentTimeMillis());}
/*Вычисление переменной delta*/
public void setTime(long t) throws Exception {
delta = new Double((t - System.currentTimeMillis()) / timeCoeff).intValue();
}

/*Функция вычисляет секунду, минуту, час, день, месяц, год из числа обозначающего UNIX.*/
public void setNewTime(long t)
{
ms = new Double(t % 1000).shortValue();
t /= 1000; //t in sec
sec = new Double(t % 60).shortValue();
t /= 60; //t in min
min = new Double(t % 60).shortValue();
t /= 60; //t in hour
year = new Double(((t / (1461L * 24L)) << 2) + 1970).shortValue();
t %= 1461L * 24L; //Hours since end of last 4 year block
for (;;) {
int h = 365 * 24;
if ((year & 3) == 0)
h += 24;
if (t < h)
break;
++year;
t -= h;
} //at end, time is number of hours into current year
hour = new Double(t % 24).shortValue();
t /= 24; //t in day
++t;
if ((year & 3) == 0) {
if (t > 60)
--t;
else if (t == 60) {
mon = 2;
day = 29;
return;
}
}
for (mon = 0; dim[mon] < t; ++mon)
t -= dim[mon];
++mon;
day = new Double(t).shortValue();
}

/*Функции формируют строку которая будет использована в команде AT+CCLK="ДД/ММ/ГГ,чч:мм:сс" */
public String modemStr() {
return to2dig(year) + '/' + to2dig(mon) + '/' + to2dig(day) + ',' +
to2dig(hour) + ':' + to2dig(min) + ':' + to2dig(sec);
}
private static String to2dig(short v) {
if (v >= 100)
v %= 100;
return (v < 10 ? "0" : "") + v;
}
}
// End Class  time
 

/*В классе SendMessageUDP реализованы все функции отвечающие за формирование UDP-сообщения и реализующие протокол обмена с сервером */
import javax.microedition.io.Connector;
import javax.microedition.io.Datagram;
import javax.microedition.io.DatagramConnection;
import com.siemens.icm.io.ATCommand;
/*Объявлен как расширение класса Thread, значит в нем будет выполняемый метод run()*/
public class SendMessageUDP extends Thread
{
private final class RFlag {public final static short
ack = 0x01, time = 0x02;}
private volatile short rFlag;
Tc65SerialPort tc65SerialPort;
ATCommand atCommand;
AnswerAnalog ResponseAnalog;
AnswerTime ResponseTime;
/*Переменная pusk используется для остановки потока SendMessageUDP. Когда она принимает значение false прекращает работать цикл в методе run(), а с ним вместе прекращает выполняться процесс запущенный классом SendMessageUDP*/
static private volatile boolean pusk;
/*Переменная done принимает значение true когда в модем получает UDP сообщение в ответ на свою дейтаграмму, а переменная err принимает значение true если после четырех попыток передачи ответное UDP сообщение так и не получено. В любом случае дальнейшая передача датаграмм модемом прекращается.*/
static private volatile boolean done, err;
private int rptcnt =Const.maxRptCnt;
/*Если GPRS соединение существует, то conn не равно null  */
private DatagramConnection conn = null;
private Datagram sd;
private Datagram rd;

time t;
private int AI0;
private int AI1;
private int TimeModem;
private int lengthDatagram;
private short idModem;

/*Конструктор класса SendMessageUDP*/
public SendMessageUDP(Tc65SerialPort tc65SerialPort, ATCommand atCommand, time t, String idNumber)
{
this.tc65SerialPort = tc65SerialPort;
this.atCommand = atCommand;
this.t=t;
ResponseAnalog= new AnswerAnalog();
ResponseTime= new AnswerTime();
puskAI(false);
idModem=Short.parseShort(idNumber);
}
/*Метод run(), у класса объявленного как поток(Thread) должен быть определен этот метод, там содержится тело потока. Когда вызывается метод  start()   интерпретатор создает новый поток для выполнения метода run(). Этот новый поток продолжает выполняться пока метод run() не закончит работу, после чего поток перестает существовать.  */
public void run() {
/*Объявлены переменные: time_send - время отправки UDP сообщения, time_teku - текущее время. */
long time_send;
long time_teku;
/*Пока цикл выполняется будет выполняться метод run()*/
while(pusk){
try{
/*Функция IzmerenieAI() получает информацию о величине сигналов на аналоговых входах модема*/
IzmerenieAI();
while(pusk){
tc65SerialPort.serialOut("Cicl begin...\n\r");
/*Классы Sender и Receiver тоже являются расширением класса Thread. Класс Sender() отвечает за процесс отправки UDP-датаграммы, а класс Receiver() за процесс приема ответной датаграммы от сервера. Если в ответной датаграмме присутствует информация о времени, то корректируется время модема */
new Sender();
new Receiver();
sleep(200);
/*Вычисляем время очередной отправки UDP сообщения на сервер путем сложения актуального времени модема и периода, взятого в классе Const*/
time_send=time.int2long(t.getTime())+Const.period;
tc65SerialPort.serialOut("time_send="+time_send+"\n\r");
/*В этом цикле происходит сравнение актуального времени модема с временем очередной отправки UDP сообщения. Когда актуальное время модема становиться больше, этот цикл прекращается и программы переходит к началу внешнего цикла (см. выше) где класс Sender  запускает процесс передачи UDP сообщения на сервер, а класс Receiver запускает процесс приема датаграммы от сервера.*/
do{
time_teku=time.int2long(t.getTime());
tc65SerialPort.serialOut("time_send="+time_send+"\n\r");
tc65SerialPort.serialOut("time_teku="+time_teku+"\n\r");
IzmerenieAI();
if(pusk==false){break;}
}while(time_teku<time_send);

String Message=idModem+" "+AI0+" "+AI1+" " + TimeModem+" "+ResponseTime.Message+"\r";
tc65SerialPort.serialOut(Message+"\n\r");
}
/*Когда переменная pusk принимает значение false, то прежде чем прекратить выполнение процесса SendMessageUDP закроем GPRS соединение*/
close();
}
catch(Exception e)
{
System.out.println(e);
}
}
}

/*Функция public присваивает значения переменной private  pusk. Эта функция объявлена также synchronized, чтобы избежать  случайного одновременного вызова этой функции, что может привести к неоднозначности переменной pusk*/
public synchronized void puskAI(boolean startAI)
{
pusk=startAI;
}


/*Функция IzmerenieAI(), используя классы ResponseAnalog и time получает данные для формирования UDP сообщения */
void IzmerenieAI()
{

try {
sleep(5000);
atCommand.send("AT^SRADC=0\r", ResponseAnalog);
sleep(1000);
AI0=ResponseAnalog.value;
tc65SerialPort.serialOut("AI0="+AI0+"\n\r");
sleep(1000);
atCommand.send("AT^SRADC=1\r", ResponseAnalog);
sleep(1000);
AI1=ResponseAnalog.value;
tc65SerialPort.serialOut("AI1="+AI1+"\n\r");
sleep(1000);
atCommand.send("AT+CCLK?\r", ResponseTime);
sleep(1000);
TimeModem=t.getTime();
// time.intTime() + delta
tc65SerialPort.serialOut("TimeModem="+TimeModem+"\n\r");
}
catch(Exception e)
{
System.out.println(e);
}
}


/*Функция GPRS_udp() формирует и передает UDP сообщение на сервер*/
private void GPRS_udp() throws Exception
{
try {
/*Функция checkOpen() проверяет существуе ли GPRS соединение, если нет то открывает его */
checkOpen();
sd.reset();
/*В дейтаграмму записывается идентификатор модема */
sd.writeShort(idModem);
/*В дейтаграмму записывается величина сигнала на первом аналоговом входе модема */
sd.writeInt(AI0);
/*В дейтаграмму записывается величина сигнала на втором аналоговом входе модема */
sd.writeInt(AI1);
/*В дейтаграмму записывается актуальное время модема */
sd.writeInt(TimeModem);
/*Передача сформированной датаграммы */
conn.send(sd);
tc65SerialPort.serialOut("UDP send...\n\r");
}
catch (Exception e) {
close();
throw e;
}
}

/*Функция receive() отвечает за прием датаграммы от сервера*/
public void receive() throws Exception {
try {
checkOpen();
rd.reset();
/*Понятно, что здесь мы устанавливаем размер ожидаемой датаграммы используя переменную  lengthDatagram. А значение этой переменной присваивается  строкой: lengthDatagram=conn.getMaximumLength();  внутри функции checkOpen()*/
rd.setLength(lengthDatagram);
conn.receive(rd);
/*Принятую дейтаграмму разбираем по полям. Сперва считываем первое поле размером short и проверяем эти данные на предмет соответствия идентификатору модема. Если проверка прошла успешно считываем данные дальше*/
try {
if (rd.readShort() == idModem)
{
/*Считываем поле данных размером short и присваиваем значение переменной rFlag*/
rFlag = rd.readShort();
/*Если в переменной rFlag присутствует флаг ответа сервера 0x01, то переменная done принимает значение true*/
if ((rFlag & RFlag.ack) != 0)
{
done=true;
tc65SerialPort.serialOut("done\n\r");
}
/*Если в переменной rFlag присутствует флаг времени 0x02, то считываем следующее поле размером int, присваиваем значение переменной timeR, используем функции класса time, чтобы откорректировать переменную delta и функцией setClock(t) установим время модема соответствующее времени сервера*/
if ((rFlag & RFlag.time) != 0)
{
int timeR=rd.readInt();
tc65SerialPort.serialOut("rd:"+timeR+"\n\r");
t.setNewTime(time.int2long(timeR));
t.setTime(time.int2long(timeR));
//delta=(t - System.currentTimeMillis())/timeCoeff)
setClock(t);
}
}
}catch (Exception e) {
System.out.println(e);
close();
throw e;
}
}
catch (Exception e) {
close();
throw e;
}
}

/*Функция закрытия GPRS соединения*/
public synchronized void close() {
if (conn != null) {
try {
conn.close();
} catch (Exception e) {}
conn = null;
}
}

/*Функция проверяет наличие GPRS соединения, в случае отсутствия открывает его, используя константы из класса Const. Также присваивает переменной lengthDatagram значение устанавливающее максимальный размер датаграммы как на передачу так и на прием*/
private synchronized void checkOpen() throws Exception {
if (conn == null) {
tc65SerialPort.serialOut("Connect GPRS...\n\r");
String openParm = "datagram://" + Const.destHost + ":" + Const.destPort+ ";" + Const.connProfile;
conn = (DatagramConnection) Connector.open(openParm);
lengthDatagram=conn.getMaximumLength();
sd = conn.newDatagram(lengthDatagram);
rd = conn.newDatagram(lengthDatagram);
}
}

/*Класс Sender используя функцию GPRS_udp() отвечает за передачу UDP сообщения. Он является примером еще одного способа организации потока в JAVA (implements Runnable). Этот способ используется, когда вместе с созданием класса сразу запускается поток, который потом сам прекратит существование. В данном случае поток созданный этим классом будет существовать пока переменная err (говорящая об отсутствии ответа от сервера после  нескольких,  эта цифра обозначена в переменной rptcnt, попыток передачи), или переменная done (говорящая о наличии ответа от сервера) не примут значение true*/
private class Sender implements Runnable {
public Sender() {new Thread(this).start();}
public void run()
{
err = false;
done=false;
rFlag = 0;
while ((!done)&&(!err)) {
for (int i = 0; i < rptcnt && (rFlag & RFlag.ack) == 0 && !err; ++i)
{
try
{
GPRS_udp();
for (int j = 0; j < Const.packetTO && (rFlag & RFlag.ack) == 0 && !err; ++j)
Thread.sleep(1000);
}
catch(Exception e)
{
System.out.println(e);
}
}
if ((rFlag & RFlag.ack) != 0)
{
done = true;
tc65SerialPort.serialOut("sender done\n\r");
break;
}
else
{
err=true;
tc65SerialPort.serialOut("sender err\n\r");
break;
}
}
//while ((!done)&&(!err))
}
}

/*Класс Receiver формирует поток тем же способом, что и класс Sender. Используя функцию receiver() он отвечает за прием и расшифровку датаграмм от сервера. Поток организованный этим классом тоже существует пока переменные err или done не примут значение true*/
private class Receiver implements Runnable {
public Receiver() {new Thread(this).start();}
public void run() {
try {
checkOpen();
} catch (Exception e1) {
e1.printStackTrace();
}
do{
try {
receive();
if(done==true){tc65SerialPort.serialOut("reciver done\n\r"); break;}
if(err==true){tc65SerialPort.serialOut("reciver err\n\r"); break;}
} catch (Exception e) {}
}while((!done) && (!err) );
}
}

/*Функция setClock(time t) используя команду AT+CCLK и данные сформированные в классе time устанавливает новое внутреннее время модема*/
public void setClock(time t) throws Exception {
atCommand.send("AT+CCLK=\"" + t.modemStr() + "\"\r");
}

}
// End Class SendMessageUDP

/*Класс Const содержит статические константы. Просто удобно, когда все константы в одном месте.*/
public class Const {
public final static int packetTO = 15;
public final static int maxRptCnt = 4;
public final static short Id_TC65AnalogMessage = 0x00ff;
public final static long period=120000;
//2min*60sec*1000milsec
public final static String destHost = "213.33.999.999";
public final static String destPort = "30000";
public final static String connProfile =
"bearer_type=gprs;access_point=internet.mts.ru;" +
"username=mts;password=mts";
//public final static String apn="internet.mts.ru";
//public final static String user="mts";
//public final static String psw="mts";
public final static String FileName="ProbaTC65new.dat";
}

 

 Чтобы программа запускалась сразу по включению модема воспользуемся уже знакомой нам AT командой: AT^SCFG с опциями: "Userware/Autostart" - разрешающей или запрещающей автозапуск, "Userware/Autostart/AppName" -  определяющей программу для автозапуска,  "Userware/Autostart/Delay" - определяющей время задержки (в 100 мс) запуска программы после включения модема.
В
HyperTerminal - е мы должны набрать следующее:
AT^SCFG="Userware/Autostart","","1"
AT^SCFG="Userware/Autostart/AppName","","a:/ProbaTC65new.jar"

Пустые кавычки в команде означают пустой пароль.

Теперь откомпилируем проект ProbaTC65new и скомпонуем java пакет так же как это было сделано с проектом HelloWorld. Из каталога deployed  загрузим образовавшиеся файлы ProbaTC65new.jar и ProbaTC65new.jad через "Проводник" в модем.

 Предположим у нас есть компьютер подключенный к интернет со статическим
IP-адресом 213.33.999.999. И на нем работает программа ServerTC65tnew .
  Чтобы продемонстрировать как использовать программу
ProbaTC65new запустим ее через HyperTerminal. Затем жмем Enter.

Так как это первый запуск программы, то файла
ProbaTC65new.dat не было. Следовательно идентификатор модема был принят по умолчанию 255 и в созданный файл ProbaTC65.dat был записан этот идентификатор. Чтобы ввести новый идентификатор жмем '4'.

Попадаем  в меню
"Change settings modem". Жмем '1' . Нам сообщается о текущем идентификаторе модема и предлагается ввести новый. Вводим 1 и жмем Enter. Появляется сообщение "New id modem: 1". Чтобы сохранить новый идентификатор модема в файле жмем '8'. Затем жмем '0', чтобы перейти в основное меню. Теперь мы готовы использовать модем в качестве измерителя аналоговых сигналов.
Жмем
'2'. И наблюдаем за работой программы ProbaTC65new в режиме передачи UDP сообщений.




Исходники рассмотренной программы
: ProbaTC65new.rar
Далее рассмотрим программу написанную на С++
Builder 6, реализующую обмен UDP сообщениями с модемом Siemens TC65 по предложенному протоколу.