GSM Modem M95 от QUECTEL — опыт освоения

в 4:27, , рубрики: Песочница, Программирование, телекоммуникации, телефония, метки: , , ,

GSM Modem M95 от QUECTEL — опыт освоения

В начале

В первых строках своего поста хочу честно признаться что ранее опыта работы с GSM-модемами у меня не было, однако волею судьбы и служебной необходимостью набрел на сайт питерской конторы «Сатрон» — официального представителя QUECTEL. На сей ресурс забрел в поисках очередной железки — подходящего мне по параметрам GSM-модема и бютжетного GPS-приемника. Так получилось что информацию о продукции в виде даташитов и прочих полезных файлов можно было получить только после регистрации. Так и сделал.

Получив подтверждение через сутки (почти расстроился так долго ждать), первым делом закачал документацию на заинтересовавший меня модуль. На следующий день мне поступил звонок с неизвестного номера, приятный женский голос сообщил что звонит менеджер фирмы «Сатрон» по поводу моей заинтересованности в их продукции. Девушка на том конце телефонного контакта поинтересовалась какой у меня статус — частное лицо, либо юридическое. На этот вопрос ответил вопросом — с какой целью интересуетесь? И случилось то, чего я в принципе не ожидал, девушка объяснила что если я представляю юридическое лицо, то выбранную мною продукцию мне вышлют для испытаний при условии что будет оплачена пересылка. Естественно я согласился, хотя девушка-менеджер сама мне порекомендовала продукцию в ответ на запрошенные параметры. В пожеланиях было получить самый примитивный но предельно дешёвый GSM-модем, с возможностью слать SMS-ки и голосовой связью. В ответ мне были порекомендованы список продукции, а на запрос по модему предложен довольно свежий продукт M95 по цене «примитива». Естественно цен на продукцию я приводить не буду, это наши с «Сатрон» отношения, но замечу что модемы и другие модули были высланы в кратчайший срок и в нескольких экземплярах.

Сразу мне показалось что такой подход несколько странный, с чего это такая щедрость. Однако далее мне стало многое понятно, и дружелюбный подход фирмы, и качественная круглосуточная поддержка, и невероятное качество работы девушки-менеджера.

Дело в том что:

Так не бывает! Весь мой опыт контактов с Российскими поставщиками компонентов вопил — это подстава и очередное «впаривание» гнилого «отстоя». Однако при беглом знакомстве с документацией на модем M95 у меня зародилось странное сомнение, уж очень прорывные характеристики у наглого представителя китайского электронпрома по сравнению с именитыми монстрами, а цена… Это уже совсем отдельная история, боюсь обвинят в рекламе.

Так вот, поскольку опыта работы с модемами у меня было ровным счетом полное зеро, а испытать и проверить соответствие заявленным характеристикам было нужно, пришлось состряпать в Altium-е схему с импульсником на 4.4 вольта а сам модем обвесить необходимой периферией включая разъемы для наушников и микрофона, кнопками ON/OFF и Emergence OFF. Антенну применил AMMAP 003. Вся конструкция согласовалась по FT232LR с USB портом персоналки, причем питание портов FT232LR взято с выхода самого модема (2.8В), так что согласовывать по уровням не пришлось.

Первый старт — порт обнаружен, настраиваю порт в программе CommTrack (пока сырая, писалась сугубо под задачу) и естественно ввожу в окне консоли «AT» и жму . Замечательно! Модем ответил эхом:

AT
OK

Далее начались непонятности. Вначале как это водится решил устроить «звонок другу» (то есть себе). Однако модем устойчиво откликался «NO CARRIER». Сказалась невнимательность при изучении доки — оказывается есть разница при вызове для голосового соединения и для цифрового подключения. К номеру нужно было подцепить ";". К моему стыду я задал вопрос контактному лицу и получил ответ от инженера в весьма краткой и доходчивой форме.

Так, звоню себе на мобильник-вижу вызов. Подтверждаю вызов и пытаюсь поговорить с самим собой — тишина. Оказывается нужно настроить канал аналоговых входов и выходов. Посылаю команду модему «AT+QAUDCH=2».
Следующая попытка дозвониться оказалась удачной, и мне удалось услышать себя же по телефону и в гарнитуре. Удивило невероятно высокое качество звука и четкость речи, отсутствие каких либо эхо и других спецэффектов дешевой связи. Громкость и чувствительность микрофона регулировались в широком диапазоне, однако заводские настройки оказались наиболее удачными.
Немного поигравшись с голосовой связью SMS-ками и освоив необходимый набор команд из арсенала терминальных и GSM, приступил к установке GPRS соединения.

IP-стек и простота в использовании

Естественно установить соединение с удаленным сервером мне помог человек, уже имевший опыт с настройкой профиля соединения, и локальная задача тестирования модема на время отклика и скорости обмена данными заданной размерности была выполнена «по шаблону». Для тех кто еще не сталкивался с M95 приведу фрагмент кода программы на VB.NET.

    Private Sub RunScript()
        Dim sRet, sTemp As String
        Dim timeStart As Date
        Dim timeStop As Date
        Dim nTime As Double = 0
        Dim nCount As Integer
        Dim tSpan As TimeSpan
        Dim serialStatus As Boolean = SerialPortA.IsOpen
        Dim seansOpen As Boolean = False
        Dim sBlock440 As String = "operations$öûâðëéîð÷ñéðöâéîðó2837âí2àãïëöãéïàöëäî3ïðàëîöíóïàùã34ïàùãà4óíïùöãíï4àêãùíöïùøãí4ïàùãöóåïàêãöù4ïàùãø4íïàùöã4íïàêùö6å4ê6åïöøãíàïö4ùïåàöùàïöùãà4ïöùã47ïåàêùöïù4ãïàøöãàï4ùãï4àùãöïàùöãï4àøùã6ï4àùãö4ïåàøãöï64àøã46ïà6öïàø76ö4ïàäöãàïíöãøíïuiowq3ygrowg3fowrtgowufygbow4gfowueyfgco6wt4go68gfrwkuyfcgwkgy4ckuw4ygfouwygou4fgrfouwf4gow4grfw4yofgowu4ygfou4fgwo4uygfwy4gfow4g4gfowufgou4rgyfwuyfguwygf4uyg4f6gfiu4gklejhdkwehfewhrkferhiuht43iuhiur"
        ' Блок переменных для фиксации временных рараметров и отказов
        Dim nDisconnect As Integer = 0
        Dim nMinTimeA, nMinTimeB, nMaxTimeA, nMaxTimeB, nEvgTimeA, nEvgTimeB As Integer
        Dim nSendAttemptA, nSendAttemptB, nLostPackA, nLostPackB As Integer
        Dim nSendSuccA, nSendSuccB As Integer
        Dim nLostConnect As Integer = 0
        Dim timeSummary As Double = 0
        Dim timeSummaryMin As Double = 0
        Dim timeSummaryMax As Double = 0
        Dim timeSummaryEvg As Double = 0
        _allowRun = True
        _allowEcho = True
        _ReadBuff = ""
        _lastClear = False
        _lastLine = ""
        nMinTimeA = 0
        nMinTimeB = 0
        nMaxTimeA = 0
        nMaxTimeB = 0
        nEvgTimeA = 0
        nEvgTimeB = 0
        nSendAttemptA = 0
        nSendAttemptB = 0
        nLostPackA = 0
        nLostPackB = 0
        nSendSuccA = 0
        nSendSuccB = 0

        'Настройка пула сообщений
        MessPull.ClearMessage()
        MessPull.AllowCMEErrorIntercept = True
        MessPull.AllowFuncIntercept = True
        MessPull.AllowSMSErrorIntercept = True
        MessPull.DropCME_Error()
        MessPull.DropSMS_Error()

        ' Начало операций
        Dim record As New StreamWriter(Application.StartupPath & "Default.log")
        Me.WriteToConsole("Script started at " + DateTime.Now.ToLongTimeString + "!" + vbCrLf)
        record.WriteLine("Script started at " + DateTime.Now.ToLongTimeString + "!")

        If Not SerialPortA.IsOpen Then
            Try
                SerialPortA.Open()
            Catch
                Me.WriteToConsole("Serial Port: " + SerialPortA.PortName + " is busy! Script stopped at: " + DateTime.Now.ToLongTimeString + "!" + vbCrLf)
                record.WriteLine("Serial Port: " + SerialPortA.PortName + " is busy! Script stopped at: " + DateTime.Now.ToLongTimeString + "!")
                _allowRun = False
            End Try
        End If

        ' Инициаллизация модема
        If _allowRun Then
            nCount = 0
            sTemp = ""
            MessPull.ClearMessage()
            While ((InStr(sTemp, "OK") = 0) And _allowRun)
                nCount += 1
                sTemp = MessPull.ExecuteAT(SerialPortA, "AT" + vbCrLf, 3000)
                Me.WriteToConsole("Attempt: " + Str(nCount) + " Send: " + sTemp)
            End While
            Me.DisplayFunc()
            ' Включаем модем
            Me.WriteToConsole("Turn on the modem!" + vbCrLf)
            If Not MessPull.CheckAndSetup(SerialPortA, "AT+CFUN?", "CFUN", "1", , , , , "AT+CFUN=1", "Call Ready", 15000) Then
                Me.WriteToConsole("Modem does not turn on! Script Aborted!" + vbCrLf)
                _allowRun = False
            Else
                ' Малозначимые настраиваем параметры
                Me.WriteToConsole(MessPull.ExecuteAT(SerialPortA, "ATE1", 3000))
                MessPull.CheckAndSetup(SerialPortA, "AT+CREG?", "CREG", "1", 0, ",", "OK", 1000, "AT+CREG=1", "OK", 5000)
                MessPull.CheckAndSetup(SerialPortA, "AT+CGREG?", "CGREG", "1", 0, ",", "OK", 1000, "AT+CGREG=1", "OK", 5000)
                MessPull.CheckAndSetup(SerialPortA, "AT+CRC?", "CRC", "0", 0, ",", "OK", 1000, "AT+CRC=0", "OK", 5000)
                MessPull.CheckAndSetup(SerialPortA, "AT+QAUDCH?", "QAUDCH", "2", 0, ",", "OK", 1000, "AT+QAUDCH=2", "OK", 5000)
                MessPull.CheckAndSetup(SerialPortA, "AT+QIFGCNT?", "QIFGCNT", "1", 0, ",", "OK", 1000, "AT+QIFGCNT=1", "OK", 5000)
                MessPull.CheckAndSetup(SerialPortA, "AT+QICSGP?", "QICSGP", "1", 0, ",", "OK", 1000, "AT+QICSGP=" + _QICSGP, "OK", 5000)
            End If
            Me.DisplayFunc()
        End If
        While _allowRun
            While _allowRun
                If (MessPull.CheckAndSetup(SerialPortA, "AT+CREG?", "CREG", "1", 1) And
                    MessPull.CheckAndSetup(SerialPortA, "AT+CGREG?", "CGREG", "1", 1)) Then
                    Exit While
                End If
                Me.DisplayFunc()
            End While
            Me.DisplayFunc()
            Me.WriteToConsole("Modem is registered on the home network at: " + DateTime.Now.ToLongTimeString + "!" + vbCrLf)
            record.WriteLine("Modem is registered on the home network at: " + DateTime.Now.ToLongTimeString + "!")
            ' Цикл установления соединения
            While (_allowRun And (Not seansOpen = True))
                Me.DisplayFunc()
                Me.WriteToConsole(MessPull.ExecuteAT(SerialPortA, "AT+QISTAT", 2000) + vbCrLf)
                'MessPull.WaitAnswer(SerialPortA, 5000, "", False)
                'record.WriteLine("Request status at: " + DateTime.Now.TimeOfDay.ToString + "!")
                sRet = MessPull.GetFuncValue("STATE")
                Select Case sRet
                    Case "IP INITIAL"
                        MessPull.CheckAndSetup(SerialPortA, "AT+QISDE?", "QISDE", "0", 0, ",", "OK", 1000, "AT+QISDE = 0", 5000)
                        MessPull.CheckAndSetup(SerialPortA, "AT+QIMUX?", "QIMUX", "0", 0, ",", "OK", 1000, "AT+QIMUX = 0", 5000)
                        MessPull.CheckAndSetup(SerialPortA, "AT+QIMODE?", "QIMODE", "0", 0, ",", "OK", 1000, "AT+QIMODE=0", 5000)
                        MessPull.CheckAndSetup(SerialPortA, "AT+QIDNSIP?", "QIDNSIP", "0", 0, ",", "OK", 1000, "AT+QIDNSIP=0", 5000)
                        If Not InStr(MessPull.ExecuteAT(SerialPortA, "AT+QIREGAPP", 1000, "OK|ERROR"), "OK") > 0 Then
                            Me.WriteToConsole("Can not register application on network! Next Attempt!" + vbCrLf)
                            record.WriteLine("Can't Activate GPRS/CSD context! Next Attempt!")
                        End If
                    Case "IP START"
                        If Not InStr(MessPull.ExecuteAT(SerialPortA, "AT+QIACT", 1000, "OK|ERROR"), "OK") > 0 Then
                            Me.WriteToConsole("Can't Activate GPRS/CSD context! Next Attempt!" + vbCrLf)
                            record.WriteLine("Can't Activate GPRS/CSD context! Next Attempt!")
                        End If
                    Case "IP GPRSACT"
                        If Not InStr(MessPull.ExecuteAT(SerialPortA, "AT+QILOCIP", 2000, "."), ".") > 0 Then
                            Me.WriteToConsole("Can't  Get local IP addres!" + vbCrLf)
                            record.WriteLine("Can't  Get local IP addres!")
                        End If
                    Case "IP STATUS"
                        If Not InStr(MessPull.ExecuteAT(SerialPortA, "AT+QIOPEN=" + _QIOPEN, 5000, "OK"), "OK") > 0 Then
                            seansOpen = True
                        Else
                            Me.WriteToConsole("Can not estabilish connect!" + vbCrLf)
                            record.WriteLine("Can not estabilish connect!")
                            seansOpen = False
                        End If
                    Case "IP CLOSE"
                        MessPull.ExecuteAT(SerialPortA, "AT+QIDEACT", 5000, "DEACT OK")
                        Me.WriteToConsole("Deactivate GPRS/CSD context!" + vbCrLf)
                        record.WriteLine("Deactivate GPRS/CSD context!")
                        seansOpen = False
                    Case "CONNECT OK"
                        seansOpen = True
                        Me.WriteToConsole("Connection OK!" + vbCrLf)
                        record.WriteLine("Connection OK!")
                    Case "PDP DEACT"
                        Me.WriteToConsole("GPRS/CSD context was deactivated because of unknown reason. Reconnecting!" + vbCrLf)
                        record.WriteLine("GPRS/CSD context was deactivated because of unknown reason. Reconnecting!")
                        If Not InStr(MessPull.ExecuteAT(SerialPortA, "AT+QIACT", 1000, "OK|ERROR"), "OK") > 0 Then
                            Me.WriteToConsole("Can't Activate GPRS/CSD context! Next Attempt!" + vbCrLf)
                            record.WriteLine("Can't Activate GPRS/CSD context! Next Attempt!")
                        End If
                    Case "TCP CONNECTING"
                        Sleep(800)
                    Case Else
                        Me.WriteToConsole("Connection status: " + sRet + vbCrLf)
                        record.WriteLine("Connection status: " + sRet)
                End Select
                Me.WriteToConsole("Connection status: " + sRet + vbCrLf)
                Me.DisplayFunc()
            End While
            While (seansOpen And _allowRun)
                MessPull.ClearMessage()
                MessPull.ExecuteAT(SerialPortA, "AT+QISTAT")
                timeSummary = 0
                nSendAttemptA += 1
                timeStart = Now
                sRet = Trim(MessPull.ExecuteAT(SerialPortA, "AT+QISEND=8", 3000, ">", False))
                'Me.WriteToConsole("Time execution AT+QISEND=8" + MessPull.LastTimeRun.ToString + vbCrLf)
                If InStr(sRet, ">") > 0 Then
                    sRet = MessPull.ExecuteAT(SerialPortA, "balance$", 3000, "SEND OK", False)
                    'Me.WriteToConsole("Time execution [balance$]" + MessPull.LastTimeRun.ToString + vbCrLf)
                    If InStr(sRet, "SEND OK") > 0 Then
                        _nCharWait = 40
                        sRet = MessPull.WaitAnswer(SerialPortA, 10000, "balance>................................", , False)
                        If sRet = "balance>................................" Then
                            Me.WriteToConsole(vbCrLf)
                            timeStop = Now
                            tSpan = timeStop.Subtract(timeStart)
                            nTime = tSpan.TotalMilliseconds
                            timeSummary = tSpan.TotalMilliseconds
                            nMinTimeA = IIf(nMinTimeA = 0, nTime, IIf(nMinTimeA > nTime, nTime, nMinTimeA))
                            nMaxTimeA = IIf(nMaxTimeA = 0, nTime, IIf(nMaxTimeA < nTime, nTime, nMaxTimeA))
                            nEvgTimeA = IIf(nEvgTimeA = 0, nTime, (nTime + nEvgTimeA) / 2)
                            nSendSuccA += 1
                            ' Теперь пытаемся выслать пакет
                            nSendAttemptB += 1
                            timeStart = Now
                            sRet = Trim(MessPull.ExecuteAT(SerialPortA, "AT+QISEND=440", 3000, ">", False))
                            'Me.WriteToConsole("Time execution AT+QISEND=440" + MessPull.LastTimeRun.ToString + vbCrLf)
                            If InStr(sRet, ">") > 0 Then
                                MessPull.ClearMessage()
                                _nCharWait = 4
                                sRet = MessPull.ExecuteAT(SerialPortA, sBlock440, 5000, "SEND OK", False)
                                If InStr(sRet, "SEND OK") > 0 Then
                                    sRet = MessPull.WaitAnswer(SerialPortA, 10000, "done", , False)
                                    If InStr(sRet, "done") > 0 Then
                                        timeStop = Now
                                        tSpan = timeStop.Subtract(timeStart)
                                        nTime = tSpan.TotalMilliseconds
                                        timeSummary = timeSummary + tSpan.TotalMilliseconds
                                        nMinTimeB = IIf(nMinTimeB = 0, nTime, IIf(nMinTimeB > nTime, nTime, nMinTimeB))
                                        nMaxTimeB = IIf(nMaxTimeB = 0, nTime, IIf(nMaxTimeB < nTime, nTime, nMaxTimeB))
                                        nEvgTimeB = IIf(nEvgTimeB = 0, nTime, (nTime + nEvgTimeB) / 2)
                                        nSendSuccB += 1
                                        Me.WriteToConsole(vbCrLf)
                                        Me.WriteToConsole("Cycle Ok. Time: " + timeSummary.ToString + vbCrLf)
                                        record.WriteLine("Cycle Ok. Time: " + timeSummary.ToString)
                                        timeSummaryMin = IIf(timeSummaryMin = 0, timeSummary,
                                                             IIf(timeSummaryMin > timeSummary, timeSummaryMin, timeSummaryMin))
                                        timeSummaryMax = IIf(timeSummaryMax = 0, timeSummary,
                                                        IIf(timeSummaryMax < timeSummary, timeSummary, timeSummaryMax))
                                        timeSummaryEvg = IIf(timeSummaryEvg = 0, timeSummary, (timeSummary + timeSummaryEvg) / 2)

                                    Else
                                        nLostPackB += 1
                                        Me.WriteToConsole("Not Answer (440 byte). Time: " + nTime.ToString + vbCrLf)
                                        record.WriteLine("Not Answer (440 byte). Time: " + nTime.ToString)
                                    End If
                                Else
                                    Me.WriteToConsole("Unsuccessfully send Data Pack (440 byte). Time: " + nTime.ToString + vbCrLf)
                                    record.WriteLine("Unsuccessfully send Data Pack (440 byte). Time: " + nTime.ToString)
                                End If
                            Else
                                timeStop = Now
                                tSpan = timeStop.Subtract(timeStart)
                                nTime = tSpan.TotalMilliseconds
                                Me.WriteToConsole("Error execute command AT+QISEND=440. Time: " + nTime.ToString + vbCrLf)
                                record.WriteLine("Error execute command AT+QISEND=440. Time: " + timeSummary.ToString)
                            End If

                        Else
                            timeStop = Now
                            tSpan = timeStop.Subtract(timeStart)
                            nTime = tSpan.TotalMilliseconds
                            nLostPackA += 1
                            Me.WriteToConsole("Unsuccessfully execute command [balance$]. Time: " + nTime.ToString + vbCrLf)
                            record.WriteLine("Unsuccessfully execute command [balance$]. Time: " + nTime.ToString)
                        End If
                    Else
                        Me.WriteToConsole("Unsuccessfully send command [balance$]. Time: " + nTime.ToString + vbCrLf)
                        record.WriteLine("Unsuccessfully send command [balance$]. Time: " + nTime.ToString)
                        MessPull.ExecuteAT(SerialPortA, "AT", 3000, "")
                        MessPull.ExecuteAT(SerialPortA, "AT", 3000, "")
                        timeStop = Now
                        tSpan = timeStop.Subtract(timeStart)
                        nTime = tSpan.TotalMilliseconds
                    End If
                Else
                    MessPull.ExecuteAT(SerialPortA, "AT", 3000, "")
                    Me.WriteToConsole("Error execute command AT+QISEND=8!" + vbCrLf)
                    record.WriteLine("Error execute command AT+QISEND=8!")
                End If
                MessPull.SetFuncValue("CounterA", nSendAttemptA.ToString)
                MessPull.SetFuncValue("CounterB", nSendAttemptB.ToString)
                MessPull.SetFuncValue("AverageTimeA", nEvgTimeA.ToString)
                MessPull.SetFuncValue("AverageTimeB", nEvgTimeB.ToString)
                Me.DisplayFunc()
                If Not MessPull.GetFuncValue("STATE") = "CONNECT OK" Then
                    nLostConnect += 1
                    seansOpen = False
                    Me.WriteToConsole("Connection lost at: " + DateTime.Now.ToLongTimeString + vbCrLf)
                    record.WriteLine("Connection lost at: " + DateTime.Now.ToLongTimeString)
                    Me.WriteToConsole("Attempt to reconnect." + vbCrLf)
                    record.WriteLine("Attempt to reconnect.")
                End If
                Sleep(2000)
            End While
        End While

        MessPull.ExecuteAT(SerialPortA, "AT+QICLOSE", 5000, "CLOSE OK|ERROR")
        MessPull.ExecuteAT(SerialPortA, "AT+QIDEACT", 5000, "DEACT OK|ERROR")
        Me.WriteToConsole("Connection Closed and GPRS/CSD context was deactivated at: " + DateTime.Now.ToLongTimeString + "!" + vbCrLf)
        record.WriteLine("Connection Closed and GPRS/CSD context was deactivated at: " + DateTime.Now.ToLongTimeString + "!")
        Me.WriteToConsole("---------------------------------------------------------------------------------------------" + vbCrLf)
        record.WriteLine("---------------------------------------------------------------------------------------------")

        Me.WriteToConsole("Execution repport:" + vbCrLf)
        record.WriteLine("Execution repport:")
        Me.WriteToConsole("[balance$] request MIN time (milliseconds): " + nMinTimeA.ToString + vbCrLf)
        record.WriteLine("[balance$] request MIN time (milliseconds): " + nMinTimeA.ToString)
        Me.WriteToConsole("[balance$] request MAX time (milliseconds): " + nMaxTimeA.ToString + vbCrLf)
        record.WriteLine("[balance$] request MAX time (milliseconds): " + nMaxTimeA.ToString)
        Me.WriteToConsole("[balance$] request AVERAGE time (milliseconds): " + nEvgTimeA.ToString + vbCrLf)
        record.WriteLine("[balance$] request AVERAGE time (milliseconds): " + nEvgTimeA.ToString)
        Me.WriteToConsole(vbCrLf)
        record.WriteLine("")
        Me.WriteToConsole("[440 byte pack] request MIN time (milliseconds): " + nMinTimeB.ToString + vbCrLf)
        record.WriteLine("[440 byte pack] request MIN time (milliseconds): " + nMinTimeB.ToString)
        Me.WriteToConsole("[440 byte pack] request MAX time (milliseconds): " + nMaxTimeB.ToString + vbCrLf)
        record.WriteLine("[440 byte pack] request MAX time (milliseconds): " + nMaxTimeB.ToString)
        Me.WriteToConsole("[440 byte pack] request AVERAGE time (milliseconds): " + nEvgTimeB.ToString + vbCrLf)
        record.WriteLine("[440 byte pack] request AVERAGE time (milliseconds): " + nEvgTimeB.ToString)
        Me.WriteToConsole(vbCrLf)
        record.WriteLine("")
        Me.WriteToConsole("Summary request MIN time (milliseconds): " + timeSummaryMin.ToString + vbCrLf)
        record.WriteLine("Summary request MIN time (milliseconds): " + timeSummaryMin.ToString)
        Me.WriteToConsole("Summary request MAX time (milliseconds): " + timeSummaryMax.ToString + vbCrLf)
        record.WriteLine("Summary request MAX time (milliseconds): " + timeSummaryMax.ToString)
        Me.WriteToConsole("Summary request AVERAGE time (milliseconds): " + timeSummaryEvg.ToString + vbCrLf)
        record.WriteLine("Summary request AVERAGE time (milliseconds): " + timeSummaryEvg.ToString)
        Me.WriteToConsole(vbCrLf)
        record.WriteLine("")
        Me.WriteToConsole("Total send attempt command [balance$]: " + nSendAttemptA.ToString + vbCrLf)
        record.WriteLine("Total send attempt command [balance$]: " + nSendAttemptA.ToString)
        Me.WriteToConsole("Total send attempt packet [440 byte]: " + nSendAttemptB.ToString + vbCrLf)
        record.WriteLine("Total send attempt packet [440 byte]: " + nSendAttemptB.ToString)
        Me.WriteToConsole("Total send succes [balance$]: " + nSendSuccA.ToString + vbCrLf)
        record.WriteLine("Total send succes [balance$]: " + nSendSuccA.ToString)
        Me.WriteToConsole("Total send succes [440 byte]: " + nSendSuccB.ToString + vbCrLf)
        record.WriteLine("Total send succes [440 byte]: " + nSendSuccB.ToString)
        Me.WriteToConsole(vbCrLf)
        record.WriteLine("")
        Me.WriteToConsole("Lost packet [balanse$]: " + nLostPackA.ToString + vbCrLf)
        record.WriteLine("Lost packet [balanse$]: " + nLostPackA.ToString)
        Me.WriteToConsole("Lost packet [440 byte]: " + nLostPackB.ToString + vbCrLf)
        record.WriteLine("Lost packet [440 byte]: " + nLostPackB.ToString)

        Me.WriteToConsole("---------------------------------------------------------------------------------------------" + vbCrLf)
        record.WriteLine("---------------------------------------------------------------------------------------------")

        Me.WriteToConsole("Script stopped at: " + DateTime.Now.ToLongTimeString + "!" + vbCrLf)
        record.WriteLine("Script stopped at: " + DateTime.Now.ToLongTimeString + "!")
        record.Close()


        'Восстановим состояние модема
        If Not serialStatus Then
            SerialPortA.Close()
        End If
        'Разрешим эхо в консоли
        _allowEcho = True
    End Sub

Это скучная часть тестирующей подпрограммы просто пояснит последовательность действий по установке соединения и настройке профиля на конкретном примере, в котором отсутствует адрес сервера и APN провайдера. Надеюсь с этим затруднений не будет.

Модем тестировался длительной поездкой по городу в подключенном режиме с посещением «мертвых» зон, где связь по телефону обрывается. Вот что удивительно, из более чем 860 пакетов, потеряно было только два, а IP-сессия вообще не прерывалась, не говоря уже о соединении. Модем тестировался в сравнении с довольно известной моделью BGS2 второй версии. Результаты тестов показали сокрушительный провал Cinterion BGS2 по устойчивости связи и скорости обмена в пользу M95. Надо сразу отметить что оба модема питались от USB порта ноутбука и имели одинаковые антенны AMMAP 003, однако для BGS2 требовался конденсатор по питанию 1000мФ, а M95 обошелся согласно документации 100 микрофарадами. При этом средний ток потребления M95 был на 46% ниже чем у BGS2. Весьма впечатляющие результаты, а если вспомнить что для написания тестовой программы для M95 у меня ушло два дня, а для BGS2 пришлось «потеть» четыре, то очевидные преимущества (по крайней мере для меня) налицо.

В общем мой первый опыт знакомства с продукцией QUECTEL в виде модема M95 весьма меня порадовал, я в первый раз столкнулся с продукцией которую делали именно для разработчиков, без абстрактного умничанья и соблюдения мертвых стандартов.

Да простят меня уважаемые читатели хабра за воспевание M95, но мне ничего подобного просто не встречалось в моей практике, и перелопаченные после этого восемь моделей совместимых GPRS модемов 12 класса убедили меня что лучше и главное дешевле, а также экономичнее и непритязательнее модема пока на российском рынке не представлено!

Сухой код

Для тех, кто пожелает использовать мои наработки, привожу сырой, но работоспособный код модуля с классом использовавшимся в примере приведенном выше а также полную версию кода формы.

Код модуля содержащего класс:

Imports System
Imports System.Object
Imports System.Collections
Imports System.Threading.Thread
Imports System.IO.Ports

Module ScriptRun

    'Public Enum AnswerStatus As Integer
    '    Reciv_Nothing = 0
    '    Reciv_OK = 1
    '    Reciv_Send = 2
    '    Reciv_Func = 4
    '    Reciv_Mess = 8
    '    Reciv_CME_ERROR = 16
    '    Reciv_ERROR = 32
    '    Reciv_TimeOut = 256
    'End Enum

    Public Class MessPull

#Region "Declaration Block"
        Private Shared msgList As String() = {}                         'массив строк сообщений, заполняемый методами класса
        '#Private Shared AnsModem As AnswerStatus                         'статус ответа из перечисления AnswerStatus
        Private Shared _funcTable As New Hashtable()                    'возвращаемые параметры функций формата +ФУНКЦИЯ: ПАРАМЕТР
        Private Shared _Recieved As Boolean                             'если принята хоть одна строка - True
        '#Private Shared _messTable As New Hashtable()                  'таблица текстовых сообщений сообщение+количество поступлений

        'разрешает перехват строк значений функций, перехваченная строка исключается из массива строк сообщений, вместо этого
        'строка разбирается на пару ключ-значение и помещается в Хэш-таблицу _funcVal
        Private Shared _funcIntercept As Boolean = True                 'Разрешает перехват функций "+FUNC: VALUE"
        Private Shared _EchoIntegcept As Boolean = True                 'Разрешает перехват эхо команды от модема (программный аналог ATE0)
        Private Shared _cmeIntercept As Boolean = True                  'Разрешает перехват собщений об ошибке "+CME ERROR: XXX"
        Private Shared _smsIntercept As Boolean = True                  'Разрешает перехват сообщений об ошибке "+SMS ERROR: XXX"
        Private Shared _cmeError As Boolean = False                     'Статус - поступила "+CME ERROR: XXX"
        Private Shared _smsError As Boolean = False                     'Статус - поступила "+SMS ERROE: XXX"
        Private Shared _cmeLastError As Integer = -1                    'Номер последней "+CME ERROR:"
        Private Shared _smsLastError As Integer = -1                    'Номер последней "+SMS ERROR:"
        Private Shared _lastCommand As String = ""                      'Последняя команда переданная модему
        ' Блок измерения времени
        Private Shared startTime, stopTime As Date                      'время вхождения в TimeOut-функцию и время завершения
        Private Shared tSpan As TimeSpan                                'обработчик времени TimeOut, измеритель времени исполнения
        Private Shared _lastTRun As Integer                             'время последнего исполнения TimeOut-функции
        Private Shared _timeOut As Boolean                              'статус TimeOut последнего исполнения TimeOut-функции
        'объект блокировки оператором SyncLock, для защиты данных от асинхронной записи
        Private Shared blockSync As New Object
        Public Shared URCMessageList() As String = {"RING", _
                                             "MO RING", _
                                             "MO CONNECTED", _
                                             "Call Ready", _
                                             "CCWV", _
                                             "RDY", _
                                             "NORMAL POWER DOWN", _
                                             "UNDER_VOLTAGE WARNING", _
                                             "UNDER_VOLTAGE POWER DOWN", _
                                             "OVER_VOLTAGE WARNING", _
                                             "OVER_VOLTAGE POWER DOWN"}
#End Region

#Region "Property definition Block"
        Public Shared ReadOnly Property FuncTable As Hashtable
            Get
                SyncLock blockSync
                    Return _funcTable
                End SyncLock
            End Get
        End Property
        Public Shared ReadOnly Property MessageRecieved As Boolean
            Get
                SyncLock blockSync
                    Return _Recieved
                End SyncLock
            End Get
        End Property
        Public Shared ReadOnly Property LastTimeRun() As Integer
            Get
                SyncLock blockSync
                    Return _lastTRun
                End SyncLock
            End Get
        End Property
        Public Shared ReadOnly Property TimeOutState() As Boolean
            Get
                SyncLock blockSync
                    Return _timeOut
                End SyncLock
            End Get
        End Property
        Public Shared Property AllowFuncIntercept() As Boolean
            Get
                SyncLock blockSync
                    Return _funcIntercept
                End SyncLock
            End Get
            Set(value As Boolean)
                SyncLock blockSync
                    _funcIntercept = value
                End SyncLock
            End Set
        End Property
        Public Shared Property AllowEchoIntercept() As Boolean
            Get
                SyncLock blockSync
                    Return _EchoIntegcept
                End SyncLock
            End Get
            Set(value As Boolean)
                SyncLock blockSync
                    _EchoIntegcept = value
                End SyncLock
            End Set
        End Property
        Public Shared Property AllowCMEErrorIntercept() As Boolean
            Get
                SyncLock blockSync
                    Return _cmeIntercept
                End SyncLock
            End Get
            Set(value As Boolean)
                SyncLock blockSync
                    _cmeIntercept = value
                End SyncLock
            End Set
        End Property
        Public Shared Property AllowSMSErrorIntercept() As Boolean
            Get
                SyncLock blockSync
                    Return _smsIntercept
                End SyncLock
            End Get
            Set(value As Boolean)
                SyncLock blockSync
                    _smsIntercept = value
                End SyncLock
            End Set
        End Property
        Public Shared ReadOnly Property LastCME_Error() As Boolean
            Get
                SyncLock blockSync
                    Return _cmeLastError
                End SyncLock
            End Get
        End Property
        Public Shared ReadOnly Property LastSMS_Error() As String
            Get
                SyncLock blockSync
                    Return _smsLastError
                End SyncLock
            End Get
        End Property
        Public Shared Property LastCommand() As String
            Get
                SyncLock blockSync
                    Return _lastCommand
                End SyncLock
            End Get
            Set(value As String)
                SyncLock blockSync
                    _lastCommand = value
                End SyncLock
            End Set
        End Property
#End Region

#Region "TimeOut-Function"
        Public Shared Function CheckAndSetup(ByRef comPort As SerialPort,
                                       ByVal sCheckCommand As String,
                                       ByVal sFunc As String,
                                       ByVal sValue As String,
                                       Optional ByVal nIndex As Integer = 0,
                                       Optional ByVal sDelim As String = ",",
                                       Optional ByVal sCheckExpect As String = "OK",
                                       Optional ByVal nCheckTimeOut As Integer = 1000,
                                       Optional ByVal sSetCommand As String = "",
                                       Optional ByVal sSetExpect As String = "OK",
                                       Optional ByVal nSetTimeOut As Integer = 2000) As Boolean
            '-----------------------------------------------------------------------------------
            'Функция ChechAndSetup посылает AT-команду <sCheckCommand> в последовательный порт <comPort> и
            'в течение определенного <nCheckTimeOut> таймаута ожидает поступления строки содержащей
            'терм "OK" или "0". После этого проверяется значение перехваченной функции
            '<sFunc> по индексу <nIndex> на соответсвие параметру <sValue>. Если остальные параметры 
            'отсутсвуют, то функция возвращает True в случае соответствия параметра иначе False.
            'Если в качестве <sCheckCommand> передана пустая строка или не открыт, функция завершится 
            'вернув False.
            '---------------------------------
            'При указании в качестве параметра <sSetCommand> не пустой строки, если результат сравнения
            'в вышеописанном алгоритме дает False, то в последовательный порт <comPort> передается 
            'комманда <sSetCommand> и производится ожидание поступление терма "OK" или "0" в
            'течение времени <nSetTimeOut>. Если до достижения таймаута получен ожидаемый терм, функция 
            'выполняет проверку значения <sFunc> по индексу <nIndex> на соответсвие параметру <sValue>.
            'Функция возвращает результат проверки на соответсвие False или True. 
            '---------------------------------
            'При возникновении таймаута функция прекращает все дальнейшие действия и всегда возврашает 
            'False выставляя состояние TimeOutState в True
            '---------------------------------
            'параметр <sDelim> определяет набор символов, который будет участвовать в парсинге
            'параметра функции методом String.Split()
            '-----------------------------------------------------------------------------------
            Dim stTime As Date = Now
            Dim sRet As String
            Dim bCheck As Boolean
            '
            startTime = stTime
            CheckAndSetup = False
            _timeOut = False
            If (comPort.IsOpen And (sCheckCommand.Length > 0)) Then
                SerialWriteLine(comPort, sCheckCommand)
                sRet = WaitAnswer(comPort, nCheckTimeOut, sCheckExpect, False)
                bCheck = CheckFuncValue(sFunc, sValue, sDelim, nIndex)
                If ((Not bCheck) And (Not _timeOut) And (sSetCommand.Length > 0)) Then
                    startTime = Now
                    SerialWriteLine(comPort, sSetCommand)
                    sRet = WaitAnswer(comPort, nSetTimeOut, sSetExpect, False)
                    If Not _timeOut Then
                        CheckAndSetup = CheckFuncValue(sFunc, sValue, sDelim, nIndex)
                    End If
                Else
                    CheckAndSetup = bCheck
                End If
            End If
            stopTime = Now
            tSpan = stopTime.Subtract(stTime)
            _lastTRun = Int(tSpan.TotalMilliseconds)
        End Function
        Public Shared Function SendData(ByRef comPort As SerialPort, _
                                         Optional sComm As String = "", _
                                         Optional ByVal tOut As Integer = 1000, _
                                         Optional sExpect As String = "OK|ERROR") As String
            '-----------------------------------------------------------------------------------
            'Функция посылает AT-команду модему подключенному к последовательному порту comPort и ждет 
            'отклика в течение времени определенным параметром sComm (в миллисекундах) или до появления
            'терма в принимаемых строках определенного списком термов sExpect.
            '------------------------------------
            'Если параметр sComm = "" или опущен, то функция пропускает действие по отсылке AT-команды
            'модему, но выполняет ожидание по времени и поиск терма в принимаемых строках. 
            '------------------------------------
            'Если параметр tOut опущен, то таймаут по умолчанию составляет 1 секунду
            '------------------------------------
            'Термы подлежащие распознаванию 'разделяются вертикальной чертой. В состав терма могут 
            'входить любые символы 'исключая вертикальную черту "|". По обнаружении терма или термов 
            'в(составе) 'принятых строк, функция возвращает строку содержущую выбранные строки до 
            'появления 'одного или нескольких термов в составе любой из строк, включая строку содержащую
            'обнаруженные термы. Выбранные строки соединяются последовательно, разделяемые символами 
            '"возврат каретки" + "перевод строки" - vbCrLf.
            'По окончании заданного времени tOut, функция возвращает выбранные до этого момента
            'строки, и выставляет состояние TimeOutState = True а в LastTimeRun заноситься общее 
            'время выполнения функции
            'При нормальном завершении функции WaitAnswer, TimeOutState сбрасывается в False
            'а в LastTimeRun заноситься время исполнения функции
            '-----------------------------------------------------------------------------------
            startTime = Now
            If sComm.Length > 0 Then
                SerialWrite(comPort, sComm)
            End If
            SendData = WaitAnswer(comPort, tOut, sExpect, False)
            stopTime = Now
            tSpan = stopTime.Subtract(startTime)
            _lastTRun = Int(tSpan.TotalMilliseconds)
        End Function
        Public Shared Function ExecuteAT(ByRef comPort As SerialPort, _
                                         Optional sComm As String = "", _
                                         Optional ByVal tOut As Integer = 1000, _
                                         Optional sExpect As String = "OK|ERROR", _
                                         Optional ByVal bSetCrLf As Boolean = True) As String
            '-----------------------------------------------------------------------------------
            'Функция посылает AT-команду модему подключенному к последовательному порту comPort и ждет 
            'отклика в течение времени определенным параметром sComm (в миллисекундах) или до появления
            'терма в принимаемых строках определенного списком термов sExpect.
            '------------------------------------
            'Если параметр sComm = "" или опущен, то функция пропускает действие по отсылке AT-команды
            'модему, но выполняет ожидание по времени и поиск терма в принимаемых строках. 
            '------------------------------------
            'Если параметр tOut опущен, то таймаут по умолчанию составляет 1 секунду
            '------------------------------------
            'Термы подлежащие распознаванию 'разделяются вертикальной чертой. В состав терма могут 
            'входить любые символы 'исключая вертикальную черту "|". По обнаружении терма или термов 
            'в(составе) 'принятых строк, функция возвращает строку содержущую выбранные строки до 
            'появления 'одного или нескольких термов в составе любой из строк, включая строку содержащую
            'обнаруженные термы. Выбранные строки соединяются последовательно, разделяемые символами 
            '"возврат каретки" + "перевод строки" - vbCrLf.
            'По окончании заданного времени tOut, функция возвращает выбранные до этого момента
            'строки, и выставляет состояние TimeOutState = True а в LastTimeRun заноситься общее 
            'время выполнения функции
            'При нормальном завершении функции WaitAnswer, TimeOutState сбрасывается в False
            'а в LastTimeRun заноситься время исполнения функции
            '-----------------------------------------------------------------------------------
            startTime = Now
            If sComm.Length > 0 Then
                comPort.WriteLine(Trim(sComm))
            End If
            ExecuteAT = WaitAnswer(comPort, tOut, sExpect, False)
            stopTime = Now
            tSpan = stopTime.Subtract(startTime)
            _lastTRun = Int(tSpan.TotalMilliseconds)
        End Function
        Public Shared Function WaitAnswer(ByRef commPort As SerialPort, _
                                          Optional tOut As Integer = 1000, _
                                          Optional sExpect As String = "OK|ERROR", _
                                          Optional ByVal bStTime As Boolean = True, _
                                          Optional ByVal bSetCrLf As Boolean = True) As String
            '-----------------------------------------------------------------------------------
            'Функция ожидает данных от модема подключенного к последовательному порту commPort в 
            'течение времени tOut в миллисекундах или до обнаружения терма в принятых строках 
            'объявленных в списке термов sExpect. 
            '------------------------------------
            'Параметр bStTime определяет необходимость измерения старового времени функции, 
            'по(умолчанию - измерять)
            '------------------------------------
            'Если параметр tOut опущен, то таймаут по умолчанию составляет 1 секунду
            '------------------------------------
            'Термы подлежащие распознаванию 'разделяются вертикальной чертой. В состав терма могут 
            'входить любые символы 'исключая вертикальную черту "|". По обнаружении терма или термов 
            'в(составе) 'принятых строк, функция возвращает строку содержущую выбранные строки до 
            'появления 'одного или нескольких термов в составе любой из строк, включая строку содержащую
            'обнаруженные термы. Выбранные строки соединяются последовательно, разделяемые символами 
            '"возврат каретки" + "перевод строки" - vbCrLf.
            'По окончании заданного времени tOut, функция возвращает выбранные до этого момента
            'строки, и выставляет состояние TimeOutState = True а в LastTimeRun заноситься общее 
            'время выполнения функции
            'При нормальном завершении функции WaitAnswer, TimeOutState сбрасывается в False
            'а в LastTimeRun заноситься время исполнения функции
            '-----------------------------------------------------------------------------------
            Dim sTemp, sText As String
            Dim sTerminator As String = IIf(bSetCrLf, vbCrLf, "")
            Dim aExpect As String() = {}
            Dim bContinue As Boolean = True

            _timeOut = False
            If bStTime Then
                startTime = Now
            End If
            WaitAnswer = ""
            If sExpect.Length > 0 Then
                aExpect = sExpect.Split("|".ToCharArray)
            Else
                aExpect = {}
            End If
            While bContinue
                While (MsgCount() < 1)
                    Sleep(5)
                    stopTime = Now
                    tSpan = stopTime.Subtract(startTime)
                    If tSpan.TotalMilliseconds > tOut Then
                        _timeOut = True
                        _lastTRun = Int(tSpan.TotalMilliseconds)
                        bContinue = False
                        Exit While
                    End If
                End While
                If bContinue Then
                    sTemp = ExtractMess()
                    WaitAnswer = IIf(WaitAnswer.Length > 0, WaitAnswer + sTemp + sTerminator, sTemp + sTerminator)
                    If aExpect.Length > 0 Then
                        For Each sText In aExpect
                            If InStr(sTemp, sText) > 0 Then
                                bContinue = False
                                Exit For
                            End If
                        Next sText
                    End If
                Else
                    Exit While
                End If
            End While
            stopTime = Now
            tSpan = stopTime.Subtract(startTime)
            _lastTRun = Int(tSpan.TotalMilliseconds)
        End Function
        Public Shared Function WaitFunc(ByVal sKey As String,
                                        Optional ByVal tOut As Integer = 1000,
                                        Optional ByVal sDelim As String = ",",
                                        Optional ByVal nIndex As Integer = -1) As String
            '-----------------------------------------------------------------------------------
            'Функция ожидает появления ключа sKey в хэш-таблице в течение времени tOut в 
            'миллисекундах. В случае орбнаружения ключа функция возвращает значение в зависимости
            'от присутствия и значения параметра nIndex.
            '------------------------------------
            'Если параметр tOut опущен, то таймаут по умолчанию составляет 1 секунду
            '------------------------------------
            'Когда параметр nIndex передан функции, производится попытка поиска параметра по индеку
            'используя в качестве разделителя группы sDelim. Если параметр nIndex опущен, функция
            'возвращает всё значение ключа sKey или пустую строку в случае его отсутствия.
            '-----------------------------------------------------------------------------------
            Dim sTemp As String
            _timeOut = False
            startTime = Now
            WaitFunc = ""
            While True
                sTemp = GetFuncValue(sKey, sDelim, nIndex)
                If sTemp.Length > 0 Then
                    WaitFunc = sTemp
                    Exit While
                End If
                stopTime = Now
                tSpan = stopTime.Subtract(startTime)
                If tSpan.TotalMilliseconds > tOut Then
                    _timeOut = True
                    _lastTRun = Int(tSpan.TotalMilliseconds)
                    Exit While
                End If
                Sleep(5)
            End While
            stopTime = Now
            tSpan = stopTime.Subtract(startTime)
            _lastTRun = Int(tSpan.TotalMilliseconds)
        End Function
#End Region
#Region "Public Methods ()"
        Public Shared Sub SerialWrite(ByVal comPort As SerialPort, ByVal sComm As String)
            If (comPort.IsOpen And (sComm.Length > 0)) Then
                comPort.Write(sComm)
                _lastCommand = Trim(sComm)
            End If
        End Sub
        Public Shared Sub SerialWriteLine(ByVal comPort As SerialPort, ByVal sComm As String)
            If (comPort.IsOpen And (sComm.Length > 0)) Then
                comPort.WriteLine(sComm)
                _lastCommand = Trim(sComm)
            End If
        End Sub
        Public Shared Function IsCME_Error() As Boolean
            IsCME_Error = _cmeError
        End Function
        Public Shared Function IsSMS_Error() As Boolean
            IsSMS_Error = _smsError
        End Function
        Public Shared Sub DropCME_Error()
            _cmeError = False
            _cmeLastError = -1
        End Sub
        Public Shared Sub DropSMS_Error()
            _smsError = False
            _smsLastError = -1
        End Sub
        Public Shared Function AppendLines(ByVal aLines As String()) As Integer
            '-------------------------------------------------------------------------------
            ' функция добавляет строки массива в массив сообщений
            ' возвращает общее количество элементов массива сообщений после добавления
            ' если в качестве аргумента указан пустой массив, то функция ничего не выполняет
            Dim sText As String
            If aLines.Length > 0 Then
                For Each sText In aLines
                    AppendLines = AppendMess(sText)
                Next
            End If
        End Function
        Public Shared Function AppendMess(ByVal sText As String) As Integer
            '-------------------------------------------------------------------------------
            ' функция добавляет строку сообщения в массив сообщений
            ' возвращает общее количество элементов массива сообщений после добавления
            ' если в качестве параметра указана пустая строка, добавления сообщения не происходит
            Dim nCount As Integer = 0
            If _funcIntercept Then
                sText = FuncIntercept(sText)
            End If
            If sText.Length > 0 Then
                SyncLock blockSync
                    nCount = msgList.Length
                    ReDim Preserve msgList(nCount)
                    msgList(nCount) = Trim(sText)
                    nCount = msgList.Length
                    _Recieved = True
                End SyncLock
            Else
                SyncLock blockSync
                    nCount = msgList.Length
                End SyncLock
            End If
            AppendMess = nCount
        End Function
        Public Shared Function Items() As String()
            '-------------------------------------------------------------------------------
            ' функция возвращает копию всего списка (массива строк) сообщений
            Dim aStr As String()
            SyncLock blockSync
                Dim nCount As Integer = UBound(msgList)
                ReDim aStr(nCount)
                Array.Copy(msgList, aStr, msgList.Length)
            End SyncLock
            Items = aStr
        End Function
        Public Shared Function MsgCount() As Integer
            '-------------------------------------------------------------------------------
            ' возвращает количество строк в массиве сообщений
            SyncLock blockSync
                MsgCount = msgList.Length
            End SyncLock
        End Function
        Public Shared Function ExtractMess() As String
            '-------------------------------------------------------------------------------
            ' функция извлекает самое первое сообщение из списка и возвращет его
            ' при этом извлекаемое сообщение удаляется из списка, а список становится 
            ' короче на одно сообщение
            ' если в списке нет ни одного сообщения, возвращается пустая строка
            Dim nCount As Integer
            ExtractMess = ""
            SyncLock blockSync
                If msgList.Length > 0 Then
                    ExtractMess = msgList(0)
                    nCount = UBound(msgList)
                    nCount -= 1
                    Array.Copy(msgList, 1, msgList, 0, msgList.Length - 1)
                    ReDim Preserve msgList(nCount)
                End If
                If msgList.Length = 0 Then _Recieved = False
            End SyncLock
        End Function
        Public Shared Sub ClearMessage()
            '-------------------------------------------------------------------------------
            ' Процедура очищает список сообщений
            SyncLock blockSync
                msgList = {}
                _Recieved = False
            End SyncLock
        End Sub

        Public Shared Function GetFuncValue(ByVal sKey As String,
                                            Optional ByVal sDelim As String = ",",
                                            Optional ByVal nIndex As Integer = -1) As String
            Dim sVal As String = ""
            Dim aVal As String() = {}
            GetFuncValue = ""
            SyncLock _funcTable.SyncRoot
                If _funcTable.ContainsKey(sKey) Then
                    sVal = _funcTable(sKey)
                End If
            End SyncLock
            If (sVal.Length > 0) And (nIndex >= 0) Then
                aVal = sVal.Split(sDelim.ToCharArray, StringSplitOptions.RemoveEmptyEntries)
                If nIndex > UBound(aVal) Then
                    Exit Function
                Else
                    GetFuncValue = aVal(nIndex)
                End If
            Else
                GetFuncValue = sVal
            End If
        End Function
        Public Shared Function CheckFuncValue(ByVal sFunc As String,
                                              ByVal sValue As String,
                                              Optional ByVal sDelim As String = ",",
                                              Optional ByVal nIndex As Integer = 0) As Boolean
            Dim aVal As String()
            CheckFuncValue = False
            If (sFunc.Length > 0) And (_funcTable.Contains(sFunc)) Then
                aVal = _funcTable(sFunc).ToString.Split(sDelim.ToCharArray, StringSplitOptions.RemoveEmptyEntries)
                If nIndex <= UBound(aVal) Then
                    CheckFuncValue = IIf(Trim(sValue) = Trim(aVal(nIndex)), True, False)
                End If
            End If
        End Function
        Public Shared Sub SetFuncValue(ByVal sKey, ByVal Value)
            SyncLock _funcTable.SyncRoot
                _funcTable(sKey) = Value
            End SyncLock
        End Sub
        Public Shared Sub RemuveFunc(ByVal sKey)
            SyncLock _funcTable.SyncRoot
                _funcTable.Remove(sKey)
            End SyncLock
        End Sub
        Public Shared Sub ClearFuncList()
            SyncLock _funcTable.SyncRoot
                _funcTable.Clear()
            End SyncLock
        End Sub
#End Region
#Region "Private Methods"
        Private Shared Function FuncIntercept(ByVal sText As String) As String
            '-------------------------------------------------------------------------------
            'Функция "перехватчик" строк типа +CREG:1. Если функция обнаруживает что
            'строка содержит именно комбинацию из лидирующего "+" и финиширующего
            'имя функции ":", то строка разбивается на имя функции и возвращаемый
            'параметр, следующий сразу после ":". Символы "+" и ":" удаляются при
            'разборе строки. 
            '----------------
            'Функция возвращает пустую строку в случае удачного перехвата или пустой
            'строки переданной в качестве параметра, иначе возвращается переданная в
            'качестве параметра строка.
            '----------------
            Dim i, n, m As Integer
            Dim aLine As String()
            Dim key As String = ""
            Dim val As String = ""
            ' Грохнем лидирующие и завершающие пробелы
            sText = Trim(sText)
            If (_EchoIntegcept And (sText = _lastCommand)) Then
                FuncIntercept = ""
                Exit Function
            End If
            ' Выставим по умолчанию возврат строки параметра
            FuncIntercept = sText
            i = InStr(sText, "+")
            n = InStr(sText, ":")
            m = InStr(sText, "^")
            If ((i > 0) Or (n > 0) Or (m > 0)) Then
                If sText.Length > 0 Then
                    aLine = sText.Split("+^:".ToCharArray, StringSplitOptions.RemoveEmptyEntries)
                    If aLine.Length = 0 Then
                        Exit Function
                    ElseIf aLine.Length = 1 Then
                        key = aLine(0)
                        val = ""
                    ElseIf aLine.Length = 2 Then
                        key = aLine(0)
                        val = aLine(1)
                    End If
                    If val = "CME ERROR" Then
                        _cmeError = True
                        _cmeLastError = Int(val)
                        If Not _cmeIntercept Then
                            Exit Function
                        End If
                    ElseIf val = "SMS ERROR" Then
                        _smsError = True
                        _smsLastError = Int(val)
                        If Not _smsIntercept Then
                            Exit Function
                        End If
                    End If
                    SyncLock _funcTable.SyncRoot
                        _funcTable(Trim(key)) = Trim(val)
                    End SyncLock
                    FuncIntercept = ""
                End If
            End If
        End Function
#End Region
    End Class


End Module

Код событий и локальных процедур/функций формы

#Region "Import Namespace"
Imports System
Imports System.Text
Imports System.IO
Imports System.IO.Ports
Imports System.Threading
Imports System.Threading.Thread
Imports System.Collections
Imports System.Object
Imports System.ComponentModel
Imports System.Runtime.InteropServices
Imports System.Runtime.ConstrainedExecution

#End Region

Public Class CommCare

#Region "Declaration"
    Shared SerialPortA As SerialPort
    Shared _continue As Boolean     ' False запрещает реакцию на изменение в ComboBox с данными настройки
    Shared _readPort As Boolean     ' False запрещает реакцию на изменение текстовых данных в консоли TextBoxM
    Shared _lineCount As Integer    ' переменная хранения количества строк в TextBoxM, если добавлена - последняя строка передается в порт
    Shared _nCharWait As Integer = 0 ' переменная указывает на то, что ожидается прием _nCharWait символов не смотря vbCrLf
    Shared _lastLine As String = "" ' хранит последние переданные в COM порт данные
    Shared _lastRead As String = "" ' накопительная переменная, в которую помещаются данные принятые из COM порта
    Shared _ReadBuff As String = "" ' перемнная в которую помещаются данные по асинхронному запросу из другого потока
    Shared _lastClear As Boolean    ' переменная указывающая что необходимо очистить _lastRead
    Shared _lockRead As Boolean     ' переменная межпотоковой блокировки опустошения или пополнения _lastRead
    Shared _allowRun As Boolean     ' переменная разрешающая продолжение исполнения скрипта, либо прерывающая (False) его исполнение
    Shared _allowEcho As Boolean    ' переменная разрешающая вывод на консоль обработчику событий приема данных из COM порта
    Shared _timeOut As Boolean      '
    Shared _QICSGP As String        ' переменная хранения настроек провайдера и сеанса связи
    Shared _QIOPEN As String        ' переменная хранения типа соединения и адреса сервера 

    Public hashToken As Hashtable   ' публичная таблица ключевых слов AT-команд
    Private ScriptThread As Thread = New Thread(AddressOf RunScript)

    ' Перечислитель элементов массива скрипта
    Public Enum scNames
        scKeyWord
        scVarData
        scLabel
        scCodeData
        scSubData
    End Enum
    ' массив элементов скрипта
    Public aScript As Array = {New Hashtable, New Hashtable, New Hashtable, New ArrayList, New ArrayList}
    Public RunStack As Stack = New Stack

    Private BaudList As String() = {"4800", "9600", "14400", "19200", "38400", "57600", "115200", "128000", "230400", "460800", "614400", "921600", "1228800"}
    Private DataBitsList As String() = {"8", "7", "6", "5", "4"}
    ' API - функция чтения параметра из INI-файла
    Private Declare Auto Function GetPrivateProfileString Lib "kernel32" _
    (ByVal lpAppName As String, _
     ByVal lpKeyName As String, _
     ByVal lpDefault As String, _
     ByVal lpReturnedString As StringBuilder, _
     ByVal nSize As Integer, _
     ByVal lpFileName As String) As Int32
    ' API - функция сохранения параметра в INI-файл
    Private Declare Auto Function WritePrivateProfileString Lib "Kernel32" _
        (ByVal Section As String, _
         ByVal Key As String, _
         ByVal putStr As String, _
         ByVal INIfile As String) As Int32
    ' This delegate enables asynchronous calls for setting
    ' the text property on a TextBox control.
    Delegate Sub SetTextCallback([text] As String, bClear As Boolean)

#End Region
#Region "COM Collectors"
    ' Recollect COM port setting 
    Private Sub Refresh_COMSetting()
        _continue = False
        ' Dim readThread As Thread = New Thread(AddressOf Read)
        ' Create a new SerialPort object with default settings.
        Call CollectParity()
        Call CollectBaudRate()
        Call DataBitsSet()
        Call StopBitsSet()
        Call FlowControlSet()
        _continue = True
    End Sub
    ' Collect list of available COM ports (registry info)
    Private Sub CollectCOMM_List()
        Dim listCOM As String() = IO.Ports.SerialPort.GetPortNames()
        Dim port As String
        ' Clear list of COM Ports
        ComboBoxCOM_List.Items.Clear()
        ' Get list of serial COM ports
        ' Fill ComboBox, names of COM
        For Each port In listCOM
            ComboBoxCOM_List.Items.Add(port)
        Next port
        ComboBoxCOM_List.SelectedIndex = 0
    End Sub
    ' Fill ComboBox BaudRate value and set ComboBox to port value allready setted 
    Private Sub CollectBaudRate()
        'Fill ComboBox_BaudList available speed list
        Dim BaudRate As String
        Dim DefBaud As Integer
        Dim DefSel As Integer
        Dim SelBox As Integer
        ComboBox_BaudList.Items.Clear()
        DefSel = 0
        DefBaud = SerialPortA.BaudRate
        For Each BaudRate In BaudList
            Try
                SerialPortA.BaudRate = Int(BaudRate)
                If Int(BaudRate) = DefBaud Then
                    SelBox = DefSel
                End If
                DefSel = DefSel + 1
                ComboBox_BaudList.Items.Add(BaudRate)
            Catch ex As Exception
                ' Do nothing

            End Try
        Next
        SerialPortA.BaudRate = DefBaud
        ComboBox_BaudList.SelectedIndex = SelBox
    End Sub
    ' Fill ComboBox DataBits value and set ComboBox to port value allready setted 
    Private Sub DataBitsSet()
        Dim DefDataBits As Integer
        Dim sBits As Integer
        Dim BoxSel, nSel As Integer
        ComboBox_DataBits.Items.Clear()
        DefDataBits = SerialPortA.DataBits
        nSel = 0
        For Each sBits In DataBitsList
            If Int(sBits) = DefDataBits Then
                BoxSel = nSel
            End If
            nSel = nSel + 1
            ComboBox_DataBits.Items.Add(sBits)
        Next
        ComboBox_DataBits.SelectedIndex = BoxSel
    End Sub
    ' Fill ComboBox StopBits value and set ComboBox to port value allready setted 
    Private Sub StopBitsSet()
        ComboBox_StopBits.Items.Clear()
        Dim s As String
        For Each s In [Enum].GetNames(GetType(StopBits))
            ComboBox_StopBits.Items.Add(s)
        Next s
        ComboBox_StopBits.SelectedIndex = SerialPortA.StopBits
    End Sub
    ' Fill ComboBox Parity value and set ComboBox to port value allready setted 
    Private Sub CollectParity()
        Dim s As String
        ComboBox_ParityList.Items.Clear()
        For Each s In [Enum].GetNames(GetType(Parity))
            ComboBox_ParityList.Items.Add(s)
        Next s
        ComboBox_ParityList.SelectedIndex = SerialPortA.Parity
    End Sub
    ' Fill ComboBox Handshake value and set ComboBox to port value allready setted 
    Private Sub FlowControlSet()
        Dim s As String
        ComboBox_FlowControl.Items.Clear()
        For Each s In [Enum].GetNames(GetType(Handshake))
            ComboBox_FlowControl.Items.Add(s)
        Next s
        ComboBox_FlowControl.SelectedIndex = SerialPortA.Parity
    End Sub
#End Region
#Region "Thread safe sub"
    ' Thread-Safe write to console TextBoxM
    Private Sub WriteToConsole(ByVal [text] As String, Optional bClear As Boolean = False)
        If Me.TextBoxM.InvokeRequired Then
            Dim d As New SetTextCallback(AddressOf WriteToConsole)
            Me.Invoke(d, New Object() {[text], bClear})
        Else
            _readPort = False
            If bClear Then
                Me.TextBoxM.Text = [text]
            Else
                Me.TextBoxM.AppendText([text])
            End If
            Me.CB_DataRecived.Checked = MessPull.MessageRecieved
        End If
    End Sub
    ' Thread-Safe write to console TextBox_Recieved
    Private Sub WriteToRecieved(ByVal [text] As String, Optional bClear As Boolean = False)
        If Me.TextBox_Recieved.InvokeRequired Then
            Dim d As New SetTextCallback(AddressOf WriteToRecieved)
            Me.Invoke(d, New Object() {[text], bClear})
        Else
            If bClear Then
                Me.TextBox_Recieved.Text = [text]
            Else
                Me.TextBox_Recieved.AppendText([text])
            End If
        End If
    End Sub
#End Region
#Region "Local procedure"
    ' Load token list from file
    Private Sub LoadToken(ByVal fName As String)
        Dim nCntr As Integer
        Dim sToken As String
        If File.Exists(fName) Then
            Dim rdStream As New StreamReader(fName)
            hashToken = New Hashtable
            nCntr = 1
            While Not rdStream.EndOfStream
                sToken = rdStream.ReadLine()
                hashToken.Add(sToken, nCntr)
                nCntr += 1
            End While
            rdStream.Close()
        End If
    End Sub
    ' Write COM port configuration to file
    Private Sub WriteConfig(ByVal fName)
        WritePrivateProfileString("SERIAL1", "PortName", ComboBoxCOM_List.Text, fName)
        WritePrivateProfileString("SERIAL1", "BaudRate", ComboBox_BaudList.Text, fName)
        WritePrivateProfileString("SERIAL1", "Parity", Str(ComboBox_ParityList.SelectedIndex), fName)
        WritePrivateProfileString("SERIAL1", "DataBits", ComboBox_DataBits.Text, fName)
        WritePrivateProfileString("SERIAL1", "StopBits", Str(ComboBox_StopBits.SelectedIndex), fName)
        WritePrivateProfileString("SERIAL1", "Handshake", Str(ComboBox_FlowControl.SelectedIndex), fName)
        WritePrivateProfileString("DATE", "LastSaved", Date.Today, fName)
        ' Form Size & Position
        WritePrivateProfileString("FORMPOS", "Y", Me.Location.Y.ToString(), fName)
        WritePrivateProfileString("FORMPOS", "X", Me.Location.X.ToString(), fName)
        WritePrivateProfileString("FORMPOS", "Height", Me.Height.ToString(), fName)
        WritePrivateProfileString("FORMPOS", "Width", Me.Width.ToString(), fName)
        ' TabControl
        WritePrivateProfileString("INTERNAL", "TabActive", TabControlA.SelectedIndex.ToString(), fName)
        WritePrivateProfileString("SCRIPT", "QICSGP", TextBoxBearer.Text, fName)
        WritePrivateProfileString("SCRIPT", "QIOPEN", TextBoxIP.Text, fName)

    End Sub
    ' Load COM port configuration from file
    Private Sub LoadConfig(ByVal fName)
        Dim nLength As Integer
        Dim sBuff As StringBuilder
        Dim iniparam As String
        Dim nIndex As Integer
        Dim aPoint As Point

        sBuff = New StringBuilder(500)
        sBuff.Clear()
        If File.Exists(fName) Then
            nLength = GetPrivateProfileString("SERIAL1", "PortName", ComboBoxCOM_List.Text, sBuff, sBuff.Capacity, fName)
            iniparam = sBuff.ToString()
            nIndex = ComboBoxCOM_List.FindString(iniparam)
            If nIndex >= 0 Then
                ComboBoxCOM_List.SelectedIndex = nIndex
                SerialPortA.PortName = ComboBoxCOM_List.Text
            End If
            sBuff.Clear()
            '
            nLength = GetPrivateProfileString("SERIAL1", "BaudRate", ComboBox_BaudList.Text, sBuff, sBuff.Capacity, fName)
            iniparam = sBuff.ToString()
            nIndex = ComboBox_BaudList.FindString(iniparam)
            If nIndex >= 0 Then
                ComboBox_BaudList.SelectedIndex = nIndex
                SerialPortA.BaudRate = Int(ComboBox_BaudList.Text)
            End If
            sBuff.Clear()
            '
            nLength = GetPrivateProfileString("SERIAL1", "Parity", Str(ComboBox_ParityList.SelectedIndex), sBuff, sBuff.Capacity, fName)
            iniparam = sBuff.ToString()
            ComboBox_ParityList.SelectedIndex = Int(iniparam)
            SerialPortA.Parity = Int(iniparam)
            sBuff.Clear()
            '
            nLength = GetPrivateProfileString("SERIAL1", "DataBits", ComboBox_DataBits.Text, sBuff, sBuff.Capacity, fName)
            iniparam = sBuff.ToString()
            nIndex = ComboBox_DataBits.FindString(iniparam)
            If nIndex >= 0 Then
                ComboBox_DataBits.SelectedIndex = nIndex
                SerialPortA.DataBits = Int(iniparam)
            End If
            sBuff.Clear()
            '
            nLength = GetPrivateProfileString("SERIAL1", "StopBits", Str(ComboBox_StopBits.SelectedIndex), sBuff, sBuff.Capacity, fName)
            iniparam = sBuff.ToString()
            ComboBox_StopBits.SelectedIndex = Int(iniparam)
            SerialPortA.StopBits = Int(iniparam)
            sBuff.Clear()
            '
            nLength = GetPrivateProfileString("SERIAL1", "Handshake", Str(ComboBox_FlowControl.SelectedIndex), sBuff, sBuff.Capacity, fName)
            iniparam = sBuff.ToString()
            ComboBox_FlowControl.SelectedIndex = Int(iniparam)
            SerialPortA.Handshake = Int(iniparam)
            ' Form size & position
            sBuff.Clear()
            nLength = GetPrivateProfileString("FORMPOS", "Y", Me.Location.Y.ToString(), sBuff, sBuff.Capacity, fName)
            aPoint.Y = Int(sBuff.ToString())
            sBuff.Clear()
            nLength = GetPrivateProfileString("FORMPOS", "X", Me.Location.X.ToString(), sBuff, sBuff.Capacity, fName)
            aPoint.X = Int(sBuff.ToString())
            Me.Location = aPoint
            sBuff.Clear()
            nLength = GetPrivateProfileString("FORMPOS", "Height", Me.Height.ToString(), sBuff, sBuff.Capacity, fName)
            Me.Height = Int(sBuff.ToString())
            sBuff.Clear()
            nLength = GetPrivateProfileString("FORMPOS", "Width", Me.Width.ToString(), sBuff, sBuff.Capacity, fName)
            Me.Width = Int(sBuff.ToString())
            ' Tab control
            sBuff.Clear()
            nLength = GetPrivateProfileString("INTERNAL", "TabActive", TabControlA.SelectedIndex.ToString(), sBuff, sBuff.Capacity, fName)
            nLength = Int(sBuff.ToString())
            TabControlA.SelectTab(nLength)
            ' Script process
            sBuff.Clear()
            nLength = GetPrivateProfileString("INTERNAL", "ScriptAutoLoad", "No", sBuff, sBuff.Capacity, fName)
            If UCase(sBuff.ToString()) = "YES" Then
                AutoLoadScriptToolStripMenuItem.Checked = True
                sBuff.Clear()
                Dim scriptPathName As String = sBuff.ToString()
                nLength = GetPrivateProfileString("INTERNAL", "ScriptFile", "", sBuff, sBuff.Capacity, fName)
                scriptPathName = sBuff.ToString()
                Dim read As New StreamReader(scriptPathName)
                TextBox_Script.Text = read.ReadToEnd.ToString
                read.Close()
            End If
            ' Load default token
            nLength = GetPrivateProfileString("SCRIPT", "TokenFileName", Application.StartupPath & "Default.tok", sBuff, sBuff.Capacity, fName)
            Call LoadToken(sBuff.ToString())
            sBuff.Clear()
            nLength = GetPrivateProfileString("SCRIPT", "QICSGP", "1,""inet.bwc.ru""", sBuff, sBuff.Capacity, fName)
            TextBoxBearer.Text = sBuff.ToString()
            sBuff.Clear()
            nLength = GetPrivateProfileString("SCRIPT", "QIOPEN", """TCP"",""46.254.241.3"",9999""", sBuff, sBuff.Capacity, fName)
            TextBoxIP.Text = sBuff.ToString()
        End If
    End Sub
    ' Check native token
    Private Function CheckNativeToken(ByVal sToken) As Boolean
        CheckNativeToken = hashToken.Contains(sToken)
    End Function
    ' Run script operation
    Private Sub RunScript()
        Dim sRet, sTemp As String
        Dim timeStart As Date
        Dim timeStop As Date
        Dim nTime As Double = 0
        Dim nCount As Integer
        Dim tSpan As TimeSpan
        Dim serialStatus As Boolean = SerialPortA.IsOpen
        Dim seansOpen As Boolean = False
        Dim sBlock440 As String = "operations$öûâðëéîð÷ñéðöâéîðó2837âí2àãïëöãéïàöëäî3ïðàëîöíóïàùã34ïàùãà4óíïùöãíï4àêãùíöïùøãí4ïàùãöóåïàêãöù4ïàùãø4íïàùöã4íïàêùö6å4ê6åïöøãíàïö4ùïåàöùàïöùãà4ïöùã47ïåàêùöïù4ãïàøöãàï4ùãï4àùãöïàùöãï4àøùã6ï4àùãö4ïåàøãöï64àøã46ïà6öïàø76ö4ïàäöãàïíöãøíïuiowq3ygrowg3fowrtgowufygbow4gfowueyfgco6wt4go68gfrwkuyfcgwkgy4ckuw4ygfouwygou4fgrfouwf4gow4grfw4yofgowu4ygfou4fgwo4uygfwy4gfow4g4gfowufgou4rgyfwuyfguwygf4uyg4f6gfiu4gklejhdkwehfewhrkferhiuht43iuhiur"
        ' Блок переменных для фиксации временных рараметров и отказов
        Dim nDisconnect As Integer = 0
        Dim nMinTimeA, nMinTimeB, nMaxTimeA, nMaxTimeB, nEvgTimeA, nEvgTimeB As Integer
        Dim nSendAttemptA, nSendAttemptB, nLostPackA, nLostPackB As Integer
        Dim nSendSuccA, nSendSuccB As Integer
        Dim nLostConnect As Integer = 0
        Dim timeSummary As Double = 0
        Dim timeSummaryMin As Double = 0
        Dim timeSummaryMax As Double = 0
        Dim timeSummaryEvg As Double = 0
        _allowRun = True
        _allowEcho = True
        _ReadBuff = ""
        _lastClear = False
        _lastLine = ""
        nMinTimeA = 0
        nMinTimeB = 0
        nMaxTimeA = 0
        nMaxTimeB = 0
        nEvgTimeA = 0
        nEvgTimeB = 0
        nSendAttemptA = 0
        nSendAttemptB = 0
        nLostPackA = 0
        nLostPackB = 0
        nSendSuccA = 0
        nSendSuccB = 0

        'Настройка пула сообщений
        MessPull.ClearMessage()
        MessPull.AllowCMEErrorIntercept = True
        MessPull.AllowFuncIntercept = True
        MessPull.AllowSMSErrorIntercept = True
        MessPull.DropCME_Error()
        MessPull.DropSMS_Error()

        ' Начало операций
        Dim record As New StreamWriter(Application.StartupPath & "Default.log")
        Me.WriteToConsole("Script started at " + DateTime.Now.ToLongTimeString + "!" + vbCrLf)
        record.WriteLine("Script started at " + DateTime.Now.ToLongTimeString + "!")

        If Not SerialPortA.IsOpen Then
            Try
                SerialPortA.Open()
            Catch
                Me.WriteToConsole("Serial Port: " + SerialPortA.PortName + " is busy! Script stopped at: " + DateTime.Now.ToLongTimeString + "!" + vbCrLf)
                record.WriteLine("Serial Port: " + SerialPortA.PortName + " is busy! Script stopped at: " + DateTime.Now.ToLongTimeString + "!")
                _allowRun = False
            End Try
        End If

        ' Инициаллизация модема
        If _allowRun Then
            nCount = 0
            sTemp = ""
            MessPull.ClearMessage()
            While ((InStr(sTemp, "OK") = 0) And _allowRun)
                nCount += 1
                sTemp = MessPull.ExecuteAT(SerialPortA, "AT" + vbCrLf, 3000)
                Me.WriteToConsole("Attempt: " + Str(nCount) + " Send: " + sTemp)
            End While
            Me.DisplayFunc()
            ' Включаем модем
            Me.WriteToConsole("Turn on the modem!" + vbCrLf)
            If Not MessPull.CheckAndSetup(SerialPortA, "AT+CFUN?", "CFUN", "1", , , , , "AT+CFUN=1", "Call Ready", 15000) Then
                Me.WriteToConsole("Modem does not turn on! Script Aborted!" + vbCrLf)
                _allowRun = False
            Else
                ' Малозначимые настраиваем параметры
                Me.WriteToConsole(MessPull.ExecuteAT(SerialPortA, "ATE1", 3000))
                MessPull.CheckAndSetup(SerialPortA, "AT+CREG?", "CREG", "1", 0, ",", "OK", 1000, "AT+CREG=1", "OK", 5000)
                MessPull.CheckAndSetup(SerialPortA, "AT+CGREG?", "CGREG", "1", 0, ",", "OK", 1000, "AT+CGREG=1", "OK", 5000)
                MessPull.CheckAndSetup(SerialPortA, "AT+CRC?", "CRC", "0", 0, ",", "OK", 1000, "AT+CRC=0", "OK", 5000)
                MessPull.CheckAndSetup(SerialPortA, "AT+QAUDCH?", "QAUDCH", "2", 0, ",", "OK", 1000, "AT+QAUDCH=2", "OK", 5000)
                MessPull.CheckAndSetup(SerialPortA, "AT+QIFGCNT?", "QIFGCNT", "1", 0, ",", "OK", 1000, "AT+QIFGCNT=1", "OK", 5000)
                MessPull.CheckAndSetup(SerialPortA, "AT+QICSGP?", "QICSGP", "1", 0, ",", "OK", 1000, "AT+QICSGP=" + _QICSGP, "OK", 5000)
            End If
            Me.DisplayFunc()
        End If
        While _allowRun
            While _allowRun
                If (MessPull.CheckAndSetup(SerialPortA, "AT+CREG?", "CREG", "1", 1) And
                    MessPull.CheckAndSetup(SerialPortA, "AT+CGREG?", "CGREG", "1", 1)) Then
                    Exit While
                End If
                Me.DisplayFunc()
            End While
            Me.DisplayFunc()
            Me.WriteToConsole("Modem is registered on the home network at: " + DateTime.Now.ToLongTimeString + "!" + vbCrLf)
            record.WriteLine("Modem is registered on the home network at: " + DateTime.Now.ToLongTimeString + "!")
            ' Цикл установления соединения
            While (_allowRun And (Not seansOpen = True))
                Me.DisplayFunc()
                Me.WriteToConsole(MessPull.ExecuteAT(SerialPortA, "AT+QISTAT", 2000) + vbCrLf)
                'MessPull.WaitAnswer(SerialPortA, 5000, "", False)
                'record.WriteLine("Request status at: " + DateTime.Now.TimeOfDay.ToString + "!")
                sRet = MessPull.GetFuncValue("STATE")
                Select Case sRet
                    Case "IP INITIAL"
                        MessPull.CheckAndSetup(SerialPortA, "AT+QISDE?", "QISDE", "0", 0, ",", "OK", 1000, "AT+QISDE = 0", 5000)
                        MessPull.CheckAndSetup(SerialPortA, "AT+QIMUX?", "QIMUX", "0", 0, ",", "OK", 1000, "AT+QIMUX = 0", 5000)
                        MessPull.CheckAndSetup(SerialPortA, "AT+QIMODE?", "QIMODE", "0", 0, ",", "OK", 1000, "AT+QIMODE=0", 5000)
                        MessPull.CheckAndSetup(SerialPortA, "AT+QIDNSIP?", "QIDNSIP", "0", 0, ",", "OK", 1000, "AT+QIDNSIP=0", 5000)
                        If Not InStr(MessPull.ExecuteAT(SerialPortA, "AT+QIREGAPP", 1000, "OK|ERROR"), "OK") > 0 Then
                            Me.WriteToConsole("Can not register application on network! Next Attempt!" + vbCrLf)
                            record.WriteLine("Can't Activate GPRS/CSD context! Next Attempt!")
                        End If
                    Case "IP START"
                        If Not InStr(MessPull.ExecuteAT(SerialPortA, "AT+QIACT", 1000, "OK|ERROR"), "OK") > 0 Then
                            Me.WriteToConsole("Can't Activate GPRS/CSD context! Next Attempt!" + vbCrLf)
                            record.WriteLine("Can't Activate GPRS/CSD context! Next Attempt!")
                        End If
                    Case "IP GPRSACT"
                        If Not InStr(MessPull.ExecuteAT(SerialPortA, "AT+QILOCIP", 2000, "."), ".") > 0 Then
                            Me.WriteToConsole("Can't  Get local IP addres!" + vbCrLf)
                            record.WriteLine("Can't  Get local IP addres!")
                        End If
                    Case "IP STATUS"
                        If Not InStr(MessPull.ExecuteAT(SerialPortA, "AT+QIOPEN=" + _QIOPEN, 5000, "OK"), "OK") > 0 Then
                            seansOpen = True
                        Else
                            Me.WriteToConsole("Can not estabilish connect!" + vbCrLf)
                            record.WriteLine("Can not estabilish connect!")
                            seansOpen = False
                        End If
                    Case "IP CLOSE"
                        MessPull.ExecuteAT(SerialPortA, "AT+QIDEACT", 5000, "DEACT OK")
                        Me.WriteToConsole("Deactivate GPRS/CSD context!" + vbCrLf)
                        record.WriteLine("Deactivate GPRS/CSD context!")
                        seansOpen = False
                    Case "CONNECT OK"
                        seansOpen = True
                        Me.WriteToConsole("Connection OK!" + vbCrLf)
                        record.WriteLine("Connection OK!")
                    Case "PDP DEACT"
                        Me.WriteToConsole("GPRS/CSD context was deactivated because of unknown reason. Reconnecting!" + vbCrLf)
                        record.WriteLine("GPRS/CSD context was deactivated because of unknown reason. Reconnecting!")
                        If Not InStr(MessPull.ExecuteAT(SerialPortA, "AT+QIACT", 1000, "OK|ERROR"), "OK") > 0 Then
                            Me.WriteToConsole("Can't Activate GPRS/CSD context! Next Attempt!" + vbCrLf)
                            record.WriteLine("Can't Activate GPRS/CSD context! Next Attempt!")
                        End If
                    Case "TCP CONNECTING"
                        Sleep(800)
                    Case Else
                        Me.WriteToConsole("Connection status: " + sRet + vbCrLf)
                        record.WriteLine("Connection status: " + sRet)
                End Select
                Me.WriteToConsole("Connection status: " + sRet + vbCrLf)
                Me.DisplayFunc()
            End While
            While (seansOpen And _allowRun)
                MessPull.ClearMessage()
                MessPull.ExecuteAT(SerialPortA, "AT+QISTAT")
                timeSummary = 0
                nSendAttemptA += 1
                timeStart = Now
                sRet = Trim(MessPull.ExecuteAT(SerialPortA, "AT+QISEND=8", 3000, ">", False))
                'Me.WriteToConsole("Time execution AT+QISEND=8" + MessPull.LastTimeRun.ToString + vbCrLf)
                If InStr(sRet, ">") > 0 Then
                    sRet = MessPull.ExecuteAT(SerialPortA, "balance$", 3000, "SEND OK", False)
                    'Me.WriteToConsole("Time execution [balance$]" + MessPull.LastTimeRun.ToString + vbCrLf)
                    If InStr(sRet, "SEND OK") > 0 Then
                        _nCharWait = 40
                        sRet = MessPull.WaitAnswer(SerialPortA, 10000, "balance>................................", , False)
                        If sRet = "balance>................................" Then
                            Me.WriteToConsole(vbCrLf)
                            timeStop = Now
                            tSpan = timeStop.Subtract(timeStart)
                            nTime = tSpan.TotalMilliseconds
                            timeSummary = tSpan.TotalMilliseconds
                            nMinTimeA = IIf(nMinTimeA = 0, nTime, IIf(nMinTimeA > nTime, nTime, nMinTimeA))
                            nMaxTimeA = IIf(nMaxTimeA = 0, nTime, IIf(nMaxTimeA < nTime, nTime, nMaxTimeA))
                            nEvgTimeA = IIf(nEvgTimeA = 0, nTime, (nTime + nEvgTimeA) / 2)
                            nSendSuccA += 1
                            ' Теперь пытаемся выслать пакет
                            nSendAttemptB += 1
                            timeStart = Now
                            sRet = Trim(MessPull.ExecuteAT(SerialPortA, "AT+QISEND=440", 3000, ">", False))
                            'Me.WriteToConsole("Time execution AT+QISEND=440" + MessPull.LastTimeRun.ToString + vbCrLf)
                            If InStr(sRet, ">") > 0 Then
                                MessPull.ClearMessage()
                                _nCharWait = 4
                                sRet = MessPull.ExecuteAT(SerialPortA, sBlock440, 5000, "SEND OK", False)
                                If InStr(sRet, "SEND OK") > 0 Then
                                    sRet = MessPull.WaitAnswer(SerialPortA, 10000, "done", , False)
                                    If InStr(sRet, "done") > 0 Then
                                        timeStop = Now
                                        tSpan = timeStop.Subtract(timeStart)
                                        nTime = tSpan.TotalMilliseconds
                                        timeSummary = timeSummary + tSpan.TotalMilliseconds
                                        nMinTimeB = IIf(nMinTimeB = 0, nTime, IIf(nMinTimeB > nTime, nTime, nMinTimeB))
                                        nMaxTimeB = IIf(nMaxTimeB = 0, nTime, IIf(nMaxTimeB < nTime, nTime, nMaxTimeB))
                                        nEvgTimeB = IIf(nEvgTimeB = 0, nTime, (nTime + nEvgTimeB) / 2)
                                        nSendSuccB += 1
                                        Me.WriteToConsole(vbCrLf)
                                        Me.WriteToConsole("Cycle Ok. Time: " + timeSummary.ToString + vbCrLf)
                                        record.WriteLine("Cycle Ok. Time: " + timeSummary.ToString)
                                        timeSummaryMin = IIf(timeSummaryMin = 0, timeSummary,
                                                             IIf(timeSummaryMin > timeSummary, timeSummaryMin, timeSummaryMin))
                                        timeSummaryMax = IIf(timeSummaryMax = 0, timeSummary,
                                                        IIf(timeSummaryMax < timeSummary, timeSummary, timeSummaryMax))
                                        timeSummaryEvg = IIf(timeSummaryEvg = 0, timeSummary, (timeSummary + timeSummaryEvg) / 2)

                                    Else
                                        nLostPackB += 1
                                        Me.WriteToConsole("Not Answer (440 byte). Time: " + nTime.ToString + vbCrLf)
                                        record.WriteLine("Not Answer (440 byte). Time: " + nTime.ToString)
                                    End If
                                Else
                                    Me.WriteToConsole("Unsuccessfully send Data Pack (440 byte). Time: " + nTime.ToString + vbCrLf)
                                    record.WriteLine("Unsuccessfully send Data Pack (440 byte). Time: " + nTime.ToString)
                                End If
                            Else
                                timeStop = Now
                                tSpan = timeStop.Subtract(timeStart)
                                nTime = tSpan.TotalMilliseconds
                                Me.WriteToConsole("Error execute command AT+QISEND=440. Time: " + nTime.ToString + vbCrLf)
                                record.WriteLine("Error execute command AT+QISEND=440. Time: " + timeSummary.ToString)
                            End If

                        Else
                            timeStop = Now
                            tSpan = timeStop.Subtract(timeStart)
                            nTime = tSpan.TotalMilliseconds
                            nLostPackA += 1
                            Me.WriteToConsole("Unsuccessfully execute command [balance$]. Time: " + nTime.ToString + vbCrLf)
                            record.WriteLine("Unsuccessfully execute command [balance$]. Time: " + nTime.ToString)
                        End If
                    Else
                        Me.WriteToConsole("Unsuccessfully send command [balance$]. Time: " + nTime.ToString + vbCrLf)
                        record.WriteLine("Unsuccessfully send command [balance$]. Time: " + nTime.ToString)
                        MessPull.ExecuteAT(SerialPortA, "AT", 3000, "")
                        MessPull.ExecuteAT(SerialPortA, "AT", 3000, "")
                        timeStop = Now
                        tSpan = timeStop.Subtract(timeStart)
                        nTime = tSpan.TotalMilliseconds
                    End If
                Else
                    MessPull.ExecuteAT(SerialPortA, "AT", 3000, "")
                    Me.WriteToConsole("Error execute command AT+QISEND=8!" + vbCrLf)
                    record.WriteLine("Error execute command AT+QISEND=8!")
                End If
                MessPull.SetFuncValue("CounterA", nSendAttemptA.ToString)
                MessPull.SetFuncValue("CounterB", nSendAttemptB.ToString)
                MessPull.SetFuncValue("AverageTimeA", nEvgTimeA.ToString)
                MessPull.SetFuncValue("AverageTimeB", nEvgTimeB.ToString)
                Me.DisplayFunc()
                If Not MessPull.GetFuncValue("STATE") = "CONNECT OK" Then
                    nLostConnect += 1
                    seansOpen = False
                    Me.WriteToConsole("Connection lost at: " + DateTime.Now.ToLongTimeString + vbCrLf)
                    record.WriteLine("Connection lost at: " + DateTime.Now.ToLongTimeString)
                    Me.WriteToConsole("Attempt to reconnect." + vbCrLf)
                    record.WriteLine("Attempt to reconnect.")
                End If
                Sleep(2000)
            End While
        End While

        MessPull.ExecuteAT(SerialPortA, "AT+QICLOSE", 5000, "CLOSE OK|ERROR")
        MessPull.ExecuteAT(SerialPortA, "AT+QIDEACT", 5000, "DEACT OK|ERROR")
        Me.WriteToConsole("Connection Closed and GPRS/CSD context was deactivated at: " + DateTime.Now.ToLongTimeString + "!" + vbCrLf)
        record.WriteLine("Connection Closed and GPRS/CSD context was deactivated at: " + DateTime.Now.ToLongTimeString + "!")
        Me.WriteToConsole("---------------------------------------------------------------------------------------------" + vbCrLf)
        record.WriteLine("---------------------------------------------------------------------------------------------")

        Me.WriteToConsole("Execution repport:" + vbCrLf)
        record.WriteLine("Execution repport:")
        Me.WriteToConsole("[balance$] request MIN time (milliseconds): " + nMinTimeA.ToString + vbCrLf)
        record.WriteLine("[balance$] request MIN time (milliseconds): " + nMinTimeA.ToString)
        Me.WriteToConsole("[balance$] request MAX time (milliseconds): " + nMaxTimeA.ToString + vbCrLf)
        record.WriteLine("[balance$] request MAX time (milliseconds): " + nMaxTimeA.ToString)
        Me.WriteToConsole("[balance$] request AVERAGE time (milliseconds): " + nEvgTimeA.ToString + vbCrLf)
        record.WriteLine("[balance$] request AVERAGE time (milliseconds): " + nEvgTimeA.ToString)
        Me.WriteToConsole(vbCrLf)
        record.WriteLine("")
        Me.WriteToConsole("[440 byte pack] request MIN time (milliseconds): " + nMinTimeB.ToString + vbCrLf)
        record.WriteLine("[440 byte pack] request MIN time (milliseconds): " + nMinTimeB.ToString)
        Me.WriteToConsole("[440 byte pack] request MAX time (milliseconds): " + nMaxTimeB.ToString + vbCrLf)
        record.WriteLine("[440 byte pack] request MAX time (milliseconds): " + nMaxTimeB.ToString)
        Me.WriteToConsole("[440 byte pack] request AVERAGE time (milliseconds): " + nEvgTimeB.ToString + vbCrLf)
        record.WriteLine("[440 byte pack] request AVERAGE time (milliseconds): " + nEvgTimeB.ToString)
        Me.WriteToConsole(vbCrLf)
        record.WriteLine("")
        Me.WriteToConsole("Summary request MIN time (milliseconds): " + timeSummaryMin.ToString + vbCrLf)
        record.WriteLine("Summary request MIN time (milliseconds): " + timeSummaryMin.ToString)
        Me.WriteToConsole("Summary request MAX time (milliseconds): " + timeSummaryMax.ToString + vbCrLf)
        record.WriteLine("Summary request MAX time (milliseconds): " + timeSummaryMax.ToString)
        Me.WriteToConsole("Summary request AVERAGE time (milliseconds): " + timeSummaryEvg.ToString + vbCrLf)
        record.WriteLine("Summary request AVERAGE time (milliseconds): " + timeSummaryEvg.ToString)
        Me.WriteToConsole(vbCrLf)
        record.WriteLine("")
        Me.WriteToConsole("Total send attempt command [balance$]: " + nSendAttemptA.ToString + vbCrLf)
        record.WriteLine("Total send attempt command [balance$]: " + nSendAttemptA.ToString)
        Me.WriteToConsole("Total send attempt packet [440 byte]: " + nSendAttemptB.ToString + vbCrLf)
        record.WriteLine("Total send attempt packet [440 byte]: " + nSendAttemptB.ToString)
        Me.WriteToConsole("Total send succes [balance$]: " + nSendSuccA.ToString + vbCrLf)
        record.WriteLine("Total send succes [balance$]: " + nSendSuccA.ToString)
        Me.WriteToConsole("Total send succes [440 byte]: " + nSendSuccB.ToString + vbCrLf)
        record.WriteLine("Total send succes [440 byte]: " + nSendSuccB.ToString)
        Me.WriteToConsole(vbCrLf)
        record.WriteLine("")
        Me.WriteToConsole("Lost packet [balanse$]: " + nLostPackA.ToString + vbCrLf)
        record.WriteLine("Lost packet [balanse$]: " + nLostPackA.ToString)
        Me.WriteToConsole("Lost packet [440 byte]: " + nLostPackB.ToString + vbCrLf)
        record.WriteLine("Lost packet [440 byte]: " + nLostPackB.ToString)

        Me.WriteToConsole("---------------------------------------------------------------------------------------------" + vbCrLf)
        record.WriteLine("---------------------------------------------------------------------------------------------")

        Me.WriteToConsole("Script stopped at: " + DateTime.Now.ToLongTimeString + "!" + vbCrLf)
        record.WriteLine("Script stopped at: " + DateTime.Now.ToLongTimeString + "!")
        record.Close()


        'Восстановим состояние модема
        If Not serialStatus Then
            SerialPortA.Close()
        End If
        'Разрешим эхо в консоли
        _allowEcho = True
    End Sub

    Private Sub InitialIpSession()
        Dim sTemp As String
        ' Select a context as foreground context AT+QIFGCNT=1
        sTemp = MessPull.ExecuteAT(SerialPortA, "AT+QIFGCNT=1", 3000)
        If sTemp = "OK" Then
            Me.WriteToConsole(sTemp + vbCrLf)
        Else
            Me.WriteToConsole("AT+QIFGCNT=1 Returns ERROR. Script stopped at:" + DateTime.Now.ToLongTimeString + "!" + vbCrLf)
            _allowRun = False
            'Exit While
        End If
        'AT+QICSGP=1,"inet.bwc.ru"
        sTemp = MessPull.ExecuteAT(SerialPortA, "AT+QICSGP=1,""inet.bwc.ru""", 3000)
        If sTemp = "OK" Then
            Me.WriteToConsole(sTemp + vbCrLf)
        Else
            Me.WriteToConsole("AT+QICSGP Returns ERROR. Script stopped at:" + DateTime.Now.ToLongTimeString + "!" + vbCrLf)
            _allowRun = False
            'Exit While
        End If
        'AT+QIMUX=0
        sTemp = MessPull.ExecuteAT(SerialPortA, "AT+QIMUX=0", 3000)
        If sTemp = "OK" Then
            Me.WriteToConsole(sTemp + vbCrLf)
        Else
            Me.WriteToConsole("AT+QIMUX=0 Returns ERROR. Script stopped at:" + DateTime.Now.ToLongTimeString + "!" + vbCrLf)
            _allowRun = False
            'Exit While
        End If
        'AT+QIMODE=0
        sTemp = MessPull.ExecuteAT(SerialPortA, "AT+QIMUX=0", 3000)
        If sTemp = "OK" Then
            Me.WriteToConsole(sTemp + vbCrLf)
        Else
            Me.WriteToConsole("AT+QIMODE=0 Returns ERROR. Script stopped at:" + DateTime.Now.ToLongTimeString + "!" + vbCrLf)
            _allowRun = False
            'Exit While
        End If
        'AT+QIDNSIP=0
        sTemp = MessPull.ExecuteAT(SerialPortA, "AT+QIDNSIP=0", 3000)
        If sTemp = "OK" Then
            Me.WriteToConsole(sTemp + vbCrLf)
        Else
            Me.WriteToConsole("AT+QIDNSIP=0 Returns ERROR. Script stopped at:" + DateTime.Now.ToLongTimeString + "!" + vbCrLf)
            _allowRun = False
            'Exit While
        End If
        'AT+QIREGAPP
        sTemp = MessPull.ExecuteAT(SerialPortA, "AT+QIREGAPP", 3000)
        If sTemp = "OK" Then
            Me.WriteToConsole(sTemp + vbCrLf)
        Else
            Me.WriteToConsole("AT+QIREGAPP Returns ERROR. Script stopped at:" + DateTime.Now.ToLongTimeString + "!" + vbCrLf)
            _allowRun = False
            'Exit While
        End If
        'AT+QIACT
        sTemp = MessPull.ExecuteAT(SerialPortA, "AT+QIACT", 3000)
        If sTemp = "OK" Then
            Me.WriteToConsole(sTemp + vbCrLf)
        Else
            Me.WriteToConsole("AT+QIACT Returns ERROR. Script stopped at:" + DateTime.Now.ToLongTimeString + "!" + vbCrLf)
            _allowRun = False
            'Exit While
        End If
        'AT+QILOCIP
        sTemp = MessPull.ExecuteAT(SerialPortA, "AT+QILOCIP", 3000)
        If sTemp = "OK" Then
            Me.WriteToConsole(sTemp + vbCrLf)
        Else
            Me.WriteToConsole("AT+QILOCIP Returns ERROR. Script stopped at:" + DateTime.Now.ToLongTimeString + "!" + vbCrLf)
            _allowRun = False
            'Exit While
        End If
    End Sub
    'Возвращает последнюю цифру в из строки цифровых значений разделенных запятой
    Private Function GetLastValue(ByVal sParam As String) As Integer
        Dim aLine As String() = sParam.Split(",".ToCharArray)
        GetLastValue = Int(aLine(UBound(aLine)))
    End Function
    ' Тестовая функция, выводит все значения возвращаемых AT-функций в консоль принытых данных
    Private Sub DisplayFunc()
        Dim enumFunc As IDictionaryEnumerator = MessPull.FuncTable.GetEnumerator
        Me.WriteToRecieved("Function parametr list:" + vbCrLf, True)
        enumFunc.Reset()
        While enumFunc.MoveNext
            Me.WriteToRecieved(enumFunc.Key + "=" + enumFunc.Value.ToString + vbCrLf)
        End While
    End Sub
    ' Parse string
    Private Function StringParse(ByVal sText As String, Optional ByVal sParser As String = " ") As String()
        Dim sParam As Char() = sParser.ToCharArray()
        StringParse = {}
        If sText.Length > 0 Then
            StringParse = sText.Split(sParam)
        End If
    End Function

    Private Sub LoadKeyWord()
        Dim htKeyWord As Hashtable
        Dim aKeyToken As System.Delegate()
        'aKeyToken.A()
        htKeyWord = aScript(scNames.scKeyWord)
        htKeyWord.Add("IF", 0)
    End Sub

    Private Sub SendToModem(ByVal sText)
        If SerialPortA.IsOpen Then
            SerialPortA.WriteLine(sText)
        Else
            TextBoxM.AppendText("Serial port: " + SerialPortA.PortName + " Is closed. Script Aborted!")
            _allowRun = False
        End If
    End Sub
#End Region

#Region "Event Handler's"
    ' Procedure handler recept any change in TextBoxM. Send command string to COM-port
    Private Sub TextBoxM_TextChanged(sender As System.Object, e As System.EventArgs) Handles TextBoxM.TextChanged
        Dim LineNum As Integer
        LineNum = Me.TextBoxM.Lines.Length
        If LineNum > 1 Then
            If (LineNum > _lineCount) Then
                If _readPort Then
                    If SerialPortA.IsOpen Then
                        _lastLine = Me.TextBoxM.Lines(LineNum - 2)
                        _lastLine = Trim(_lastLine)
                        SerialPortA.WriteLine(_lastLine + vbCrLf)
                    Else
                        _readPort = False
                        Me.TextBoxM.AppendText("Serial port " + SerialPortA.PortName + " serial port is closed, open it first!" + vbCrLf)
                    End If
                Else
                    _readPort = True
                End If
                _lineCount = Me.TextBoxM.Lines.Length
            End If
        Else
            _lineCount = Me.TextBoxM.Lines.Length
        End If
    End Sub
    ' Clear text in TextBoxM on button click
    Private Sub BtClearText_Click(sender As System.Object, e As System.EventArgs) Handles BtClearText.Click
        TextBoxM.Clear()
        TextBoxM.Refresh()
        _lineCount = 0
    End Sub
    ' Change port Name on ComboBox changes
    Private Sub ComboBoxCOM_List_SelectedIndexChanged(sender As System.Object, e As System.EventArgs) Handles ComboBoxCOM_List.SelectedIndexChanged
        If _continue Then
            Call Refresh_COMSetting()
            TSLabelCOM.Text = ComboBoxCOM_List.Text
            SerialPortA.PortName = ComboBoxCOM_List.Text
        End If

    End Sub
    ' Change port Parity on ComboBox changes
    Private Sub ComboBox_ParityList_SelectedIndexChanged(sender As System.Object, e As System.EventArgs) Handles ComboBox_ParityList.SelectedIndexChanged
        If _continue Then
            Try
                SerialPortA.Parity = ComboBox_ParityList.SelectedIndex
            Catch ex As Exception
                'None
                MsgBox("Can't set selected parity." + vbCrLf, MsgBoxStyle.Exclamation, AcceptButton)
            End Try
        End If
    End Sub
    ' Change port Baud Rate on ComboBox changes
    Private Sub ComboBox_BaudList_SelectedIndexChanged(sender As System.Object, e As System.EventArgs) Handles ComboBox_BaudList.SelectedIndexChanged
        If _continue Then
            SerialPortA.BaudRate = Int(ComboBox_BaudList.Text)
            TSLabelSpeed.Text = ComboBox_BaudList.Text
        End If
    End Sub
    ' Change port Data Bits on ComboBox changes
    Private Sub ComboBox_DataBits_SelectedIndexChanged(sender As System.Object, e As System.EventArgs) Handles ComboBox_DataBits.SelectedIndexChanged
        If _continue Then
            SerialPortA.DataBits = Int(ComboBox_DataBits.Text)
        End If
    End Sub
    ' Change port Stop Bits on ComboBox changes
    Private Sub ComboBox_StopBits_SelectedIndexChanged(sender As System.Object, e As System.EventArgs) Handles ComboBox_StopBits.SelectedIndexChanged
        If _continue Then
            SerialPortA.StopBits = ComboBox_StopBits.SelectedIndex
        End If
    End Sub
    ' Change port Handshake on ComboBox changes
    Private Sub ComboBox_FlowControl_SelectedIndexChanged(sender As System.Object, e As System.EventArgs) Handles ComboBox_FlowControl.SelectedIndexChanged
        If _continue Then
            SerialPortA.Handshake = ComboBox_FlowControl.SelectedIndex
        End If
    End Sub
    ' Check COM port setting & write text to TextBox
    Private Sub BTCheck_Click(sender As System.Object, e As System.EventArgs) Handles BTCheck.Click
        TextBox1.AppendText("Serial port name: " + SerialPortA.PortName + vbCrLf)
        'TextBox1.AppendText("Serial port num: " + Str(_serialPort.PortName) + vbCrLf)
        TextBox1.AppendText("Serial port status: " + IIf(SerialPortA.IsOpen, "Port Open", "Port Closed") + vbCrLf)
        TextBox1.AppendText("Baud Rate: " + SerialPortA.BaudRate.ToString + vbCrLf)
        TextBox1.AppendText("Parity: " + SerialPortA.Parity.ToString + vbCrLf)
        TextBox1.AppendText("Parity Num: " + Str(SerialPortA.Parity) + vbCrLf)
        TextBox1.AppendText("Data Bits: " + SerialPortA.DataBits.ToString + vbCrLf)
        TextBox1.AppendText("Stop Bits: " + SerialPortA.StopBits.ToString + vbCrLf)
        TextBox1.AppendText("Flow Control: " + SerialPortA.Handshake.ToString + vbCrLf)
        TextBox1.AppendText("Read Timeout: " + SerialPortA.ReadTimeout.ToString + vbCrLf)
        TextBox1.AppendText("Write Timeout: " + SerialPortA.WriteTimeout.ToString + vbCrLf)
        TextBox1.AppendText("-------------------------" + vbCrLf + vbCrLf)
    End Sub
    ' Clear text recieved in TextBox 
    Private Sub BTClearRecieved_Click(sender As System.Object, e As System.EventArgs) Handles BTClearRecieved.Click
        TextBox_Recieved.Clear()
        _lastRead = ""
    End Sub
    ' Event handlers on form close. 
    Private Sub CommCare_FormClosed(sender As System.Object, e As System.Windows.Forms.FormClosedEventArgs) Handles MyBase.FormClosed
        If SerialPortA.IsOpen Then SerialPortA.Close()
    End Sub
    ' Open/Close serial port
    Private Sub BtSerial_Click(sender As System.Object, e As System.EventArgs) Handles BtSerial.Click
        If SerialPortA.IsOpen Then
            Try
                SerialPortA.DiscardInBuffer()
                SerialPortA.DiscardOutBuffer()
                SerialPortA.Close()
            Catch ex As Exception

            End Try
            TSLastCommand.Text = "Serial port: " + SerialPortA.PortName.ToString + " closed!"
            BtSerial.Text = "Open Port"
        Else
            Try
                SerialPortA.Open()
                SerialPortA.ReadTimeout = 1000
                SerialPortA.WriteTimeout = 1000
                If SerialPortA.IsOpen Then
                    BtSerial.Text = "Close Port"
                    TSLastCommand.Text = "Serial port: " + SerialPortA.PortName.ToString + " open successfully!"
                Else
                    BtSerial.Text = "Open Port"
                    TSLastCommand.Text = "Can't open serial port: " + SerialPortA.PortName.ToString
                End If
            Catch ex As Exception

            End Try
        End If
    End Sub
    ' Save COM port setting on menu click
    Private Sub SaveSettingToolStripMenuItem_Click(sender As System.Object, e As System.EventArgs) Handles SaveSettingToolStripMenuItem.Click
        Dim saveDialog As New SaveFileDialog
        Dim fName As String
        saveDialog.InitialDirectory = Application.StartupPath
        saveDialog.Filter = "INI Files (*.ini)|*.ini"
        saveDialog.FileName = "Setting.ini"
        If saveDialog.ShowDialog = Windows.Forms.DialogResult.Cancel Then Exit Sub
        fName = saveDialog.InitialDirectory & "" & saveDialog.FileName
        Call WriteConfig(fName)
    End Sub
    ' Save at-protocol text from TextBox to file
    Private Sub SaveATProtocolToolStripMenuItem_Click(sender As System.Object, e As System.EventArgs) Handles SaveATProtocolToolStripMenuItem.Click
        Dim saveDialog As New SaveFileDialog
        saveDialog.InitialDirectory = Application.StartupPath
        saveDialog.Filter = "Log Files (*.log)|*.log|All Files (*.*)|*.*"
        saveDialog.FileName = "CommCare.log"
        If saveDialog.ShowDialog = Windows.Forms.DialogResult.Cancel Then Exit Sub
        Dim record As New StreamWriter(saveDialog.FileName)
        record.Write(TextBoxM.Text)
        record.Close()
    End Sub
    ' Load at-protocol text into TextBox from file
    Private Sub LoapATProtocolToolStripMenuItem_Click(sender As System.Object, e As System.EventArgs) Handles LoapATProtocolToolStripMenuItem.Click
        Dim openDialog As New OpenFileDialog
        openDialog.InitialDirectory = Application.StartupPath
        openDialog.Filter = "Log Files (*.log)|*.log|All Files (*.*)|*.*"
        openDialog.FileName = "CommCare.log"
        If openDialog.ShowDialog = Windows.Forms.DialogResult.Cancel Then Exit Sub
        Dim read As New StreamReader(openDialog.FileName)
        TextBoxM.Text = read.ReadToEnd.ToString
        read.Close()
    End Sub
    ' Load form Events. Proceed initialisation
    Private Sub CommCare_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
        Dim fName, defParam, retParam As String
        _lastRead = ""                          ' Empty _lastRead
        _allowEcho = True                       ' Permit echo the received data handler
        CB_DataRecived.Checked = False          ' Set visual data recieved check
        ' Collect serial port info adn declare it
        _continue = False                       ' prevent reactions to fill ComboBox
        SerialPortA = New SerialPort()          ' Create serial port object
        ' Define handler to recieved data
        AddHandler SerialPortA.DataReceived, AddressOf DataReceviedHandler

        'SerialPortA.ReadTimeout = 1000          ' set Read timeout 1000mS
        'SerialPortA.WriteTimeout = 1000         ' set Write timeout 1000mS
        Call CollectCOMM_List()                 ' Collect COM port Names
        ' Set port name first line ComboBox
        SerialPortA.PortName = ComboBoxCOM_List.Text
        Call Refresh_COMSetting()               ' Collect the remaining serial port settings
        ' Complete fragments of the status bar
        TSLabelSpeed.Text = ComboBox_BaudList.Text
        TSLabelCOM.Text = ComboBoxCOM_List.Text
        _readPort = True                        ' Allow the reaction to the emergence of a new line in TextBoxM
        ' Read default config file
        fName = Application.StartupPath & "Default.ini"
        Call LoadConfig(fName)
        _continue = True                        ' Allow the reaction to changes in the ComboBox's
        WritePrivateProfileString("INTERNAL", "ScriptSaved", "No", fName)
        WritePrivateProfileString("INTERNAL", "ScriptSaved", "No", fName)
        '
        TextBox_Recieved.ReadOnly = True
    End Sub
    ' Close form events. Execute finishing action
    Private Sub CommCare_FormClosing(sender As System.Object, e As System.Windows.Forms.FormClosingEventArgs) Handles MyBase.FormClosing
        Dim fName As String
        fName = Application.StartupPath & "Default.ini"
        Call WriteConfig(fName)
        If Not (ScriptThread.ThreadState = Threading.ThreadState.Unstarted) Then
            ScriptThread.Abort()
            ScriptThread.Join(1000)
        End If
    End Sub
    ' Load COM port setting from file
    Private Sub OpenSettingToolStripMenuItem_Click(sender As System.Object, e As System.EventArgs) Handles OpenSettingToolStripMenuItem.Click
        Dim openDialog As New OpenFileDialog
        Dim fName As String
        openDialog.InitialDirectory = Application.StartupPath
        openDialog.Filter = "INI Files (*.ini)|*.ini"
        openDialog.FileName = "Setting.ini"
        If openDialog.ShowDialog = Windows.Forms.DialogResult.Cancel Then Exit Sub
        fName = openDialog.InitialDirectory & "" & openDialog.FileName
        _continue = False
        Call LoadConfig(fName)
        _continue = True
    End Sub
    ' Test button Parse click. Go parse testing string
    Private Sub BT_Parse_Click(sender As System.Object, e As System.EventArgs) Handles BT_Parse.Click
        Dim words() As String
        Dim word As String
        Dim textStr As String
        Dim delim As Char()
        Dim parseString As String
        parseString = TB_InStr.Text
        textStr = TB_Delimiters.Text
        delim = textStr.ToCharArray()
        words = parseString.Split(delim, StringSplitOptions.RemoveEmptyEntries)
        For Each word In words
            TB_Console.AppendText(word + vbCrLf)
        Next
    End Sub
    ' Test button Clear click. Clear TextBox text.
    Private Sub BT_Clear_Click(sender As System.Object, e As System.EventArgs) Handles BT_Clear.Click
        TB_Console.Clear()
    End Sub
    ' CheckBox prevent manually changes
    Private Sub CB_DataRecived_CheckedChanged(sender As System.Object, e As System.EventArgs) Handles CB_DataRecived.CheckedChanged
        CB_DataRecived.Checked = MessPull.MessageRecieved
    End Sub
    ' Load token from file
    Private Sub LoadTokenToolStripMenuItem_Click(sender As System.Object, e As System.EventArgs) Handles LoadTokenToolStripMenuItem.Click
        Dim openDialog As New OpenFileDialog
        openDialog.InitialDirectory = Application.StartupPath
        openDialog.Filter = "Token Files (*.tok)|*.tok|All Files (*.*)|*.*"
        'openDialog.FileName = "Default.tok"
        If openDialog.ShowDialog = Windows.Forms.DialogResult.Cancel Then Exit Sub
        Call LoadToken(openDialog.FileName)
        WritePrivateProfileString("SCRIPT", "TokenFileName", openDialog.FileName, Application.StartupPath & "Default.ini")
    End Sub
    ' Menu File-Exit
    Private Sub ExitToolStripMenuItem_Click(sender As System.Object, e As System.EventArgs) Handles ExitToolStripMenuItem.Click
        Application.Exit()
    End Sub
    ' Script Save to file
    Private Sub SaveTaskScriptToolStripMenuItem_Click(sender As System.Object, e As System.EventArgs) Handles SaveTaskScriptToolStripMenuItem.Click
        Dim saveDialog As New SaveFileDialog
        saveDialog.InitialDirectory = Application.StartupPath
        saveDialog.Filter = """Go"" Script Files (*.gos)|*.gos|All Files (*.*)|*.*"
        saveDialog.FileName = "Noname.gos"
        If saveDialog.ShowDialog = Windows.Forms.DialogResult.Cancel Then Exit Sub
        Dim record As New StreamWriter(saveDialog.FileName)
        record.Write(TextBox_Script.Text)
        record.Close()
        WritePrivateProfileString("INTERNAL", "ScriptSaved", "Yes", Application.StartupPath & "Default.ini")
        WritePrivateProfileString("INTERNAL", "ScriptFile", saveDialog.FileName, Application.StartupPath & "Default.ini")
    End Sub
    ' Script Load from file
    Private Sub LoadTaskScriptToolStripMenuItem_Click(sender As System.Object, e As System.EventArgs) Handles LoadTaskScriptToolStripMenuItem.Click
        Dim openDialog As New OpenFileDialog
        openDialog.InitialDirectory = Application.StartupPath
        openDialog.Filter = """Go"" Script Files (*.gos)|*.gos|All Files (*.*)|*.*"
        'openDialog.FileName = "CommCare.gos"
        If openDialog.ShowDialog = Windows.Forms.DialogResult.Cancel Then Exit Sub
        Dim read As New StreamReader(openDialog.FileName)
        TextBox_Script.Text = read.ReadToEnd.ToString
        read.Close()
        WritePrivateProfileString("INTERNAL", "ScriptLoaded", "Yes", Application.StartupPath & "Default.ini")
        WritePrivateProfileString("INTERNAL", "ScriptFile", openDialog.FileName, Application.StartupPath & "Default.ini")
    End Sub
    ' Auto load script check/uncheck
    Private Sub AutoLoadScriptToolStripMenuItem_Click(sender As System.Object, e As System.EventArgs) Handles AutoLoadScriptToolStripMenuItem.Click
        AutoLoadScriptToolStripMenuItem.Checked = True
        WritePrivateProfileString("INTERNAL", "ScriptAutoLoad", "Yes", Application.StartupPath & "Default.ini")
    End Sub
#End Region
#Region "COM port Handler's"
    Private Sub DataReceviedHandler(sender As Object, e As SerialDataReceivedEventArgs)
        Dim sp As SerialPort = CType(sender, SerialPort)
        Dim aLine As String()
        Dim nAt As Integer
        Dim sParam As Char()
        Dim sTemp As String
        Dim indata As String = ""

        indata = sp.ReadExisting()
        '
        sTemp = vbCrLf
        sParam = sTemp.ToCharArray()
        '
        _lastRead = _lastRead + indata
        'If _lastRead.Length > 1 Then
        'If _lastRead = "> " Then
        'MessPull.AppendMess(Trim(_lastRead))
        '_lastRead = ""
        'End If
        'End If
        nAt = InStr(_lastRead, vbCrLf)
        While nAt > 0
            MessPull.AppendMess(Mid(_lastRead, 1, nAt - 1))
            _lastRead = Mid(_lastRead, nAt + 2)
            nAt = InStr(_lastRead, vbCrLf)
        End While
        If (_nCharWait > 0) And (_lastRead.Length = _nCharWait) Then
            MessPull.AppendMess(Trim(_lastRead))
            _lastRead = ""
            _nCharWait = 0
        ElseIf _lastRead = "> " Then
            MessPull.AppendMess(Trim(_lastRead))
            _lastRead = ""
        End If
        'If nAt > 0 Then
        'aLine = _lastRead.Split(sParam, StringSplitOptions.RemoveEmptyEntries)
        'If aLine.Length > 0 Then
        'MsPull.AppendLines(aLine)
        '_lastRead = ""
        'End If
        'End If
        '
        If _allowEcho Then
            Call Me.WriteToConsole(indata)
        End If
    End Sub
#End Region

    Private Sub BTRunStop_Click(sender As System.Object, e As System.EventArgs) Handles BTRunStop.Click
        If _allowRun Then
            _allowRun = False
            BTRunStop.Text = "Run Script"
        Else
            _lastRead = ""
            CB_DataRecived.Checked = MessPull.MessageRecieved
            _allowRun = True
            BTRunStop.Text = "Stop Script"
            ScriptThread = New Thread(AddressOf RunScript)
            TimerA.Interval = 10000
            TimerA.Enabled = True
            _timeOut = False
            ScriptThread.Start()
        End If
    End Sub

    Private Sub TimerA_Tick(sender As System.Object, e As System.EventArgs) Handles TimerA.Tick
        If Not _timeOut Then _timeOut = True
    End Sub

    Private Sub BTTestMsg1_Click(sender As System.Object, e As System.EventArgs) Handles BTTestMsg1.Click
        Dim nCount As Integer = MessPull.AppendMess(TB_InStr.Text)
        TSLastCommand.Text = "Message count: " + Str(nCount)
    End Sub

    Private Sub BTTestMsg2_Click(sender As System.Object, e As System.EventArgs) Handles BTTestMsg2.Click
        Dim sText As String = TB_InStr.Text
        Dim sParam As String = " ,.;:!@#$%&*()-=+|/?<>"
        Dim aParam As Char() = sParam.ToCharArray()
        TSLastCommand.Text = "Message count: " + Str(MessPull.AppendLines(sText.Split(aParam, StringSplitOptions.RemoveEmptyEntries)))
    End Sub

    Private Sub BTTestMsg3_Click(sender As System.Object, e As System.EventArgs) Handles BTTestMsg3.Click
        Dim alines As String() = MessPull.Items()
        Dim sText As String
        For Each sText In alines
            TB_Console.AppendText(sText + vbCrLf)
        Next
    End Sub

    Private Sub BTTestMsg4_Click(sender As System.Object, e As System.EventArgs) Handles BTTestMsg4.Click
        TB_InStr.Text = MessPull.ExtractMess()
        TSLastCommand.Text = "Message count: " + Str(MessPull.MsgCount())
    End Sub

    Private Sub BTTestMsgClear_Click(sender As System.Object, e As System.EventArgs) Handles BTTestMsgClear.Click
        MessPull.ClearMessage()
    End Sub

    Private Sub TextBoxBearer_TextChanged(sender As System.Object, e As System.EventArgs) Handles TextBoxBearer.TextChanged
        _QICSGP = TextBoxBearer.Text
    End Sub

    Private Sub TextBoxIP_TextChanged(sender As System.Object, e As System.EventArgs) Handles TextBoxIP.TextChanged
        _QIOPEN = TextBoxIP.Text
    End Sub
End Class

Всем наилучшего! Если вы найдете в исходном коде для себя полезные фрагменты — буду рад что оказался полезен!

Автор: StixNematic

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js