Как правильно работать с GSM модулем (да и любым устройством по serial) без delay?
- Войдите на сайт для отправки комментариев
Считаю, что код в стиле:
gsmSerial.println("ATH");
delay(5000);
gsmSerial.println("ATD>1;");
delay(30000);
gsmSerial.println("ATD>2;");
не совсем правильный, хоть и прост, и правильнее было бы работать без delay, но это достаточно сложно, так как нужно сначала запросить что-то, потом ждать ответ и правильно его соотнести, так как может быть ответ сначала на другой запрос или вообще например сообщение о низком заряде аккумулятора, а ждешь статус модуля допустим, поэтому включенное ЭХО может и пригодиться (если я правильно понял его работу).
В общем набросал звонок на первый номер, и уже каша в коде, а это еще нет проверки ответа, занято, линия недоступна или сброшен вызов, или все ок, и т.п., кто как считает, как правильнее позвонить допустим, но чтобы все учесть, если вдруг сейчас идет звонок или модуль вообще завис и не отвечает, значит его перезагрузить и т.п.
Тут в коде идет проверка статуса модуля и если он не готов к звонку, то положить трубку если идет разговор или запросить попозже, если модуль чем-то занят, если нет ответа более 5 сек., то еще запросить и так 5 раз, если ничего непомогло перезагрузка модуля, и всё по-новой через 20 сек. Если же модуль готов, то позвонить на первый номер, дальше нет кода, но будет уже проверка ответа на запрос звонка (занят, нет сети, нет ответа и т.д.), если что звонок на второй номер, или также перезагрузка и т.д. и т.п.
void loop() {
static unsigned long currentMillis = millis();
static boolean alarm = false;
static byte alarmMode = 0;
static unsigned long alarmPreviousMillis = 0;
if(какое-то условие)
alarm = true;
if(gsmSerial.available())
{
char inChar = (char)gsmSerial.read();
if(inChar == '\r')
{
if(gsmString.startsWith("+CPAS:")) // текущее состояние модуля
{
if(alarmMode == 1 || alarmMode == 3 || alarmMode == 5 || alarmMode == 7 || alarmMode == 9)
{
String pasString = gsmString.substring(7); // 0 – готовность к работе, 2 - ответ и выполнение команд не гарантируется, 3 - идет входящий звонок, 4 установлено голосовое соединение
if(pasString == "0")
{
gsmSerial.println("ATD>1;"); // позвонить на номер в первой ячейке СИМ
alarmMode = 21;
alarmPreviousMillis = currentMillis;
}
else if(pasString == "3" || pasString == "4")
{
gsmSerial.println("ATH"); // завершить все соединения
alarmMode = 11;
alarmPreviousMillis = currentMillis;
}
else //2
{
if(alarmMode < 9)
{
alarmMode++;
alarmPreviousMillis = currentMillis;
}
else
{
pinMode(rstGSMPin, OUTPUT); // сброс модуля
delay(500);
digitalWrite(rstGSMPin, INPUT);
alarmMode = 10;
alarmPreviousMillis = currentMillis;
}
}
}
}
else if(gsmString == "OK")
{
if(alarmMode == 11)
{
gsmSerial.println("ATD>1;"); // позвонить на номер в первой ячейке СИМ
alarmMode = 21;
alarmPreviousMillis = currentMillis;
}
}
gsmString = "";
}
else if (inChar != '\n')
{
gsmString += inChar;
}
}
if(alarm)
{
if(alarmMode == 0)
{
gsmSerial.println("AT+CPAS"); // запросить текущее состояние модуля
alarmMode = 1;
alarmPreviousMillis = currentMillis;
}
else if(alarmMode >= 1 && alarmMode <= 8)
{
if(currentMillis - alarmPreviousMillis >= 5000)
{
gsmSerial.println("AT+CPAS"); // запросить текущее состояние модуля
alarmMode += alarmMode%2 + 1;
alarmPreviousMillis = currentMillis;
}
}
else if(alarmMode == 9 || alarmMode == 11)
{
if(alarmMode == 9 && currentMillis - alarmPreviousMillis >= 5000
|| alarmMode == 11 && currentMillis - alarmPreviousMillis > 20000)
{
pinMode(rstGSMPin, OUTPUT); // сброс модуля
delay(500);
digitalWrite(rstGSMPin, INPUT);
alarmMode = 10;
alarmPreviousMillis = currentMillis;
}
}
else if(alarmMode == 10)
{
if(currentMillis - alarmPreviousMillis >= 20000)
{
alarmMode = 0;
alarmPreviousMillis = currentMillis;
}
}
}
}
Вижу чтение одного байта в 14 строке и много записи.
Что такое gsmString? Вижу много работы с этим объектом на чтение, но ничиге в него не пишется.
Вот как хреновый руководитель: орать умеет, а слушать не хочет.
Кричишь в Serial команду и в цикле ждешь ответ, считая период ожидания при этом. Если нарвался на таймаут ответа, то рапортуешь об ошибке.
Вот поэтому и спросил, потому что чувствую, что есть более правильный путь, только какой? Или все так, только граф нарисовать и по нему продолжать писать?
Что такое gsmString? Вижу много работы с этим объектом на чтение, но ничиге в него не пишется.
Туда складываются байты, а когда это становится полной строкой, то разбирается, что за сообщение пришло.
Граф - это половина успеха, он легко перекладывается в код.
Andy, Допустим, то что написано в первом посте написано по графу, то алгоритм нормальный или Вы что-нибудь переделали? Подскажите, пожалуйста, просто хочется сразу правильно писать.
То что написано, к графу состояний не имеет отношения, это не более, чем реализация алгоритма из кубиков и ромбиков. Надо описать именно состояния устройства.
готовность к работе, идет входящий вызов, установлено голосовое соединение - это все состояния.
Если Вы имеете ввиду переменную, в которой находится значение в каком состоянии сейчас устройство до момента когда захочется позвонить, то если бы она была, допустим был входящий звонок и он отвечен, то все равно в этой переменной будет то что сейчас звонок, а завершен он или нет, неизвестно, но можно конечно следить за этим наверное, еще не проверял, но даже если можно, надежнее проверить все равно, как тут 5 раз и делается, точнее 1 раз точно, а если что-то пошло не так, то еще попытки. Как же по-вашему будет выглядеть алгоритм на словах или в коде, хотя бы первые итерации так сказать, а то непонятно что не так и что бы Вы поменяли...
Прмерно так:
switch (состояние) { case 0: if (условие) {gsmSerial.print("что-то"); состояние=1;} if (условие) {gsmSerial.print("что-то другое"); состояние=2; } break; case 1: if (условие | условие) {timer.Start(ххх); состояние=3;} break; case 2: и т.д. default: break }В любом случае надо сначала описать все состояния устройства, условия переходов из состояния в состояние и действия выполняемые при этом.
Так у меня тоже самое написано, только на if, потому что в данном случае гибче, типа "if(alarmMode >= 1 && alarmMode <= 8)", чем case 1: case 2: ... case 8: и вообще можно любые условия ставить.
Удачи.
Ну чтож сделал на if, но понял что с цифрами не очень удобно работать, и как раз для этого есть enum, назвал все цифры словами так сказать, стало удобнее, и переделал в итоге на switch, так как все равно других условий нет, кроме alarmMode, а также там и компилятор проверяет если что не так, например не все переменные из enum указаны.
Очень неправильный подход. Первое: сразу переделывайте на serialEvent - это сильно лучше и грамотнее. Второе - конечный автомат вас спасёт. Главное условие - никаких блокировок, это, можно сказать, залог успеха и защиты от потери данных вследствие переполнения буфера RX.
В общем случае алгоритм прост: в serialEventN (смотря какой хардварный Serial вы пользуете) вычитываете данные построчно. Как придёт строка - только тогда обрабатываете её. Обработали, поняли, что нужно сделать - сменили состояние автомата, чтобы при следующем заходе в loop выполнить требуемое действие. Псевдокод:
String buff; void serialEvent1() { char ch; while(Serial1.available()) { ch = (char) Serial1.read(); if(ch == '\r') continue; if(ch == '\n') { ProcessAnswer(buff); buff = ""; } else buff += ch; } } void ProcessAnswer(const String& line) { if(line == "OK") { switch(machineState) { case waitingForRegistration: // ждали регистрации в сети commandQueue.push(wantToSendSMS); // говорим, что на следующем заходе надо послать SMS break; } } }Сходу всего не напишешь, поэтому советую почитать информацию по конечным автоматам ;)
Я не на меге делаю, и аппаратный Serial оставил для логирования, хотя и надо только пока тестируется, но и с программным все в принципе работает. Да и прошивать проще, можно не отключать от модуля.
Возможно Вы думаете, что serialEvent использует прерывания и поэтому рекомендуете (извините если не так), но на самом деле это просто функция вызывается каждый раз в loop, то есть, что сам напишешь, что ее вызывать, все равно, а то может и больше затрат, так как еще функция вызывается, а не сразу код. Но в любом случае в SoftwareSerial все равно нет этой функции...
Ну а по коду, я примерно так и делаю, только без вызова фукнции, а прямо в коде... И переделал с вложенных состояний в ответах, наоборот сначала проверка состояний, а потом внутри уже от ответа "плясать", посчитал так удобней. Опытным путем экономлю память, правда пока хватает, просто заранее на всякий случай, разница и в вызове фукнции, если она один раз вызывается и передача переменной по ссылке против глобальной, все влияет...
> Возможно Вы думаете, что serialEvent использует прерывания и поэтому рекомендуете (извините если не так), но на самом деле это просто функция вызывается каждый раз в loop, то есть, что сам напишешь
Читать до полного просветления.
https://www.arduino.cc/en/Reference/SerialEvent
https://www.arduino.cc/en/Tutorial/SerialEvent
Возможно Вы думаете, что serialEvent использует прерывания и поэтому рекомендуете (извините если не так), но на самом деле это просто функция вызывается каждый раз в loop, то есть, что сам напишешь, что ее вызывать, все равно, а то может и больше затрат, так как еще функция вызывается, а не сразу код.
Я в курсе, спасибо. Но использовать serialEvent всё равно грамотнее, чем просто вычитывать самому по while и ждать, не находите? Накладные расходы грамотный компилятор, к слову, сведёт к минимуму, это раз. Два - функция сама вызывается только тогда, когда в порту есть данные - уже удобно, не надо специально ничего предпринимать. Три - использование serialEvent позволяет избавиться от головняков вида "блин, и вот тут надо проверять, есть ли данные из порта!" Ну и, наконец - использование такого подхода позволит легко и быстро, при необходимости, переписать кусок кода на прерывания, никак не затрагивая ваш конечный автомат.
Скажем, опять же, из опыта - тупой код в лоб с вычитыванием данных из одного порта до посинения, пока в другой приходят данные - приводит к потере данных из второго порта вследствие переполнения буфера. В этом контексте serialEvent можно рассматривать как таймслоты, которые делят между собой порты. Мне каицца, что именно для этого всё и было задумано ;)
Radjah, а что читать, то,
вот то что я и говорю:
А вот код из исходников Arduino:
int main(void) { init(); initVariant(); #if defined(USBCON) USBDevice.attach(); #endif setup(); for (;;) { loop(); if (serialEventRun) serialEventRun(); } return 0; }DIYMan, спасибо, вероятно, в моем коде лучше if(gsmSerial.available()), заменить на while, чтобы считывалось за раз если есть и знать, что бесконечно данных не будет. Но даже с serialEvent с while внутри, как Вы и написали, данные будут считываться пока не кончатся, даже если в другие сериалы придет что-то, и также данные потеряются. Так как функции все вызываются по порядку, пока не отработает предыдущая.
Но даже с serialEvent с while внутри, как Вы и написали, данные будут считываться пока не кончатся, даже если в другие сериалы придет что-то, и также данные потеряются. Так как функции все вызываются по порядку, пока не отработает предыдущая.
Всё правильно вы пишете, но не принимаете во внимание один нюанс, при работе с несколькими портами. Допустим, в первый порт мы послали команду и нам надо дождаться ответа. Псевдокод:
SendCommand("ATE0"); while(1) { if(Serial.available() && Serial.find("OK")) break; } // тут что-то делаем дальшеТо такой вот подход с while наглухо блокирует работу с остальными портами, пока на нужном порту вы не вычитаете ответ. Кстати - довольно распространённый подход среди негодных интернет-примеров ;)
В результате - потеря данных на других портах. Если же юзать банальный serialEvent, то всё простейшим образом расщепляется на таймслоты, т.е. - послали команду, а в нужном serialEvent вычитываем информацию, и когда получаем ответ - делаем что-то. При этом имеем возможность автоматически работать с другими портами, конечно, при условии грамотной организации кода, как вы правильно заметили - если висеть почём зря и ждать, когда порт разродится данными - не поможет и serialEvent.
Просто я считаю, что раз есть достаточно удобный инструмент, уже дающий необходимую обвязку - то грех им не пользоваться. Хотя - не настаиваю ни капли, каждый пляшет как умеет.
В результате - потеря данных на других портах. Если же юзать банальный serialEvent, то всё простейшим образом расщепляется на таймслоты, т.е. - послали команду, а в нужном serialEvent вычитываем информацию, и когда получаем ответ - делаем что-то. При этом имеем возможность автоматически работать с другими портами, конечно, при условии грамотной организации кода, как вы правильно заметили - если висеть почём зря и ждать, когда порт разродится данными - не поможет и serialEvent.
Просто я считаю, что раз есть достаточно удобный инструмент, уже дающий необходимую обвязку - то грех им не пользоваться. Хотя - не настаиваю ни капли, каждый пляшет как умеет.
пример проблемного кода увидел, суть понял.
а можно дать пример(лучше два :) ) как нужно делать в таком случае, наглядный пример он полезнее, спасибо.
пример проблемного кода увидел, суть понял.
а можно дать пример(лучше два :) ) как нужно делать в таком случае, наглядный пример он полезнее, спасибо.
Пример с serialEvent:
String receiveBuff; // строка, куда будем складывать данные, пока не вычитаем до \r\n void serialEvent() { while(Serial.available()) { char ch = Serial.read(); if(ch == '\r') continue; if(ch == '\n') { Process(receiveBuff); receiveBuff = "";} else receiveBuff += ch; yield(); // даём поработать коду, который требует срочного внимания. } } void Process(const String& line) { // тут обрабатываем строку, которая пришла от модуля }Впрочем, serialEvent - это не прерывания, конечно. Для действительно неблокирующей другие порты вычитки надо либо дописывать HardwareSerial, либо - вовсе отказываться от него и обрабатывать прерывания ручками.
Обратите внимание на использование функции yield - это хороший тон для обеспечения кооперативной работы. Если вы пишете библиотеку или класс, который внутри себя может выполнять длительную работу и при этом хотите, чтобы другие части кода тоже могли что-то делать, то в нужных местах ставите вызов функции yield. По сути - это пустой вызов, но в скетче вы можете определить эту функцию, написав
void yield() { }и прописав туда нужный код. Естественно, что в приведённом примере внутри функции yield не стоит вызывать serialEvent, иначе - стек попортите рекурсией.
Короче - подходов есть много разных, и в документации они прописаны. Жаль, что никто её не читает :)
Доброго дня.
Ветка конечно заброшенная, но ответ на неё мне кажется актуальным и для меня не найденным, все ответы в данной теме были плавно переключены не на избавление от delay, а на парсинг из serial, а судя по названию темы и вопросу создателя в первом сообщении, как правильно опрашивать gsm модем без использования delay, т.к. при опросе нужно делать задержки и они могут быть разными т.к. например есть функция запроса баланса
void balance () { String balanceval; String inputstr; Serial.println("AT+CUSD=1,#100#,15"); delay (4000); while(Serial.available()) { char ch=Serial.read(); if( ch == '\r' ) { if(inputstr.indexOf(F("+CUSD:"))>-1) { int p1 = inputstr.indexOf(F("\"")); int p2 = inputstr.lastIndexOf(F("\"")); inputstr = inputstr.substring(p1+1,p2); String decodestr; Decode7bit(inputstr, decodestr); balanceval = decodestr; sendTextMessage(balanceval); } } else inputstr+=ch; } }в ней после запроса стоит delay на ожидание прихода данных от оператора, и в этот момент МК зависает, вместо того что-бы делать полезную работу.
Так вот вопрос Как оптимизировать функцию что б избавиться от delay?
PS. Есть предположение что её можно разбить путём выноса
if(inputstr.indexOf(F("+CUSD:"))>-1) { int p1 = inputstr.indexOf(F("\"")); int p2 = inputstr.lastIndexOf(F("\"")); inputstr = inputstr.substring(p1+1,p2); String decodestr; Decode7bit(inputstr, decodestr); balanceval = decodestr; sendTextMessage(balanceval); }в основной блок опроса Serial крутящийся в loop.
Подтвердите моё предположение или есть более грамотное решение?
PS2 Ещё есть ф-ция отправки сообщения
void sendTextMessage(String txt) { Serial.print("AT+CMGS="); Serial.print((char)34); Serial.print("+7**********"); Serial.print((char)34); //phone number Serial.print((char)13); delay(100); Serial.println(txt); delay(100); Serial.print((char)26); Serial.print((char)13); }в ней тоже имеется 2 delay (пусть крохотные, но есть) и если их убрать то иногда некорректно отрабатывает (правда было проверенно на SoftwareSerial).
все ответы в данной теме были плавно переключены не на избавление от delay, а на парсинг из serial
да, именно так.
необходимо полностью переосмыслить работу с потоком данных, обрабатывать не всю принятую цепочку данных (да она тупо и не влезет в память ардуины), а побайтно или по логическим строкам/словам.
Так вот вопрос Как оптимизировать функцию что б избавиться от delay?
Никак, полностью переписать (см.выше).
вот вам пример перекодировки SMS , обратите внимание идет последовательная обработка входящих данных
void getDecode7bitChar(byte byte1, byte byte0) { byte posIdx = smsIdxCurrentChar % 8; byte byteOut; if (posIdx > 0) { byteOut = byte0 << posIdx; byte b2 = byte1 >> (8 - posIdx); byteOut = byteOut | b2; byteOut = byteOut & 0x7F; if (byte1) { #if (SHOW_SMS == 1) Serial.write(byteOut); #endif // find external command byte bc = findCmdInBuf(byteOut); if (bc) execExtCmd(bc - 1); } if (posIdx == 6) { byteOut = byte0 >> 1; #if (SHOW_SMS == 1) Serial.write(byteOut); #endif // find external command byte bc = findCmdInBuf(byteOut); if (bc) execExtCmd(bc - 1); ++smsIdxCurrentChar; } } else { byteOut = byte0; byteOut = byteOut & 0x7F; #if (SHOW_SMS == 1) Serial.write(byteOut); #endif // find external command byte bc = findCmdInBuf(byteOut); if (bc) execExtCmd(bc - 1); } ++smsIdxCurrentChar; }а вот например отправка СМС, аналогично побайтно берется отправляемая строка и в каждом loop цикле (образно) отправляется уже байтв кодированном виде в модем:
byte smsOutPackedByte() { switch (sms_out_pos_mode) { case 0: { // SCA if (sms_out_pos_other) { ++sms_out_pos_mode; sms_out_pos_other = 0; return '0'; } else { ++sms_out_pos_other; return '0'; } break; } case 1: { // PDU Type if (sms_out_pos_other) { ++sms_out_pos_mode; sms_out_pos_other = 0; return '1'; } else { ++sms_out_pos_other; return '0'; } break; } case 2: { // MR if (sms_out_pos_other) { ++sms_out_pos_mode; sms_out_pos_other = 0; return '0'; } else { ++sms_out_pos_other; return '0'; } break; } case 3: { // DA if (sms_out_pos_other < 2) { // lenght sender // lenth to HEX if (sms_out_pos_other) { ++sms_out_pos_other; byte tb = (smsLenSender % 16); if (tb > 9) return (tb + 0x37); else return (tb + '0'); } else { ++sms_out_pos_other; return ((smsLenSender / 16) + '0'); } } else if (sms_out_pos_other < 4) { // format sender = 91 if (sms_out_pos_other == 2) { ++sms_out_pos_other; return '9'; } else { ++sms_out_pos_other; return '1'; } } else { // sender if (sms_out_pos_sender < smsLenSender) { ++sms_out_pos_other; ++sms_out_pos_sender; if (bitRead(sms_out_pos_sender, 0)) { if ((sms_out_pos_sender >= smsLenSender) && (bitRead(smsLenSender, 0))) return ('F'); else return (smsSender[sms_out_pos_sender]); } else { return (smsSender[sms_out_pos_sender - 2]); } } else { sms_out_pos_other = 0; ++sms_out_pos_mode; if (bitRead(smsLenSender, 0)) return (smsSender[smsLenSender - 1]); else return 0xFF; } } break; } case 4: { // PID if (sms_out_pos_other) { ++sms_out_pos_mode; sms_out_pos_other = 0; return '0'; } else { ++sms_out_pos_other; return '0'; } break; } case 5: { // DCS if (sms_out_pos_other) { ++sms_out_pos_mode; sms_out_pos_other = 0; return '8'; } else { ++sms_out_pos_other; return '0'; } break; } case 6: { // UDL if (sms_out_pos_other) { ++sms_out_pos_mode; sms_out_pos_other = 0; byte uh = smsUDL % 16; if (uh > 9) return (uh + 0x37); else return (uh + '0'); } else { ++sms_out_pos_other; byte uh = smsUDL / 16; if (uh > 9) return (uh + 0x37); else return (uh + '0'); } break; } case 7: { // UD if (sms_out_pos_string < smsLenString) { byte outChar = sms_buf[sms_out_pos_string]; byte bpos = (sms_out_pos_other % 4); switch (bpos) { case 0: { ++sms_out_pos_other; return '0'; break; } case 1: { ++sms_out_pos_other; if (outChar < 0x7F) return '0'; else return '4'; break; } case 2: { ++sms_out_pos_other; if (outChar < 0x7F) { // ascii outChar = (outChar / 16); if (outChar > 9) return (outChar + 0x37); else return (outChar + '0'); } else { // win1251 switch (outChar) { case 168: { return '0'; break; } case 184: { return '5'; break; } default: { outChar -= 0xB0; outChar = (outChar / 16); if (outChar > 9) return (outChar + 0x37); else return (outChar + '0'); } } } break; } case 3: { ++sms_out_pos_other; ++sms_out_pos_string; if (outChar < 0x7F) { // ascii outChar = (outChar % 16); if (outChar > 9) return (outChar + 0x37); else return (outChar + '0'); } else { // win1251 switch (outChar) { case 168: { return '1'; break; } case 184: { return '1'; break; } default: { outChar -= 0xB0; outChar = (outChar % 16); if (outChar > 9) return (outChar + 0x37); else return (outChar + '0'); } } } break; } default: {} } } else { ++sms_out_pos_mode; sms_out_pos_other = 0; return 0; // 0xFF for next step } break; } default: { return 0; }; } }Спасибо, смысл понятен, буду устраивать себе мозговой штурм.
PS У меня в буфер Serial всё и не помещалось, ну я и пошёл неправильным путём, увеличил размер буфера!
ну я и пошёл неправильным путём, увеличил размер буфера!
ну...для начала это правильный путь, потом уже с опытом все короче и короче делается.
в идеале размер буфера не должен быть больше одной принимаемой команды.
и да, старайтесь не использовать тип String, привыкайте к массиву char.
вот немного кривой но рабочий пример, буфер 24 байта
http://arduino.ru/forum/apparatnye-voprosy/vse-o-sim800l-i-vse-chto-s-ni...
Ветка конечно заброшенная, но ответ на неё мне кажется актуальным и для меня не найденным, все ответы в данной теме были плавно переключены не на избавление от delay, а на парсинг из serial, а судя по названию темы и вопросу создателя в первом сообщении, как правильно опрашивать gsm модем без использования delay,
Именно поэтому новички, зачастую, не могут правильно сформулировать вопрос, - их знаний даже на половину ответа не хватает.
И именно поэтому более опытные участники форума "плано переключают" обсуждение темы с неправильно заданного вопроса на тот, который следовало бы задать как правильный.
Другими словами, правильный ответ, как правило, не является буквальным ответом на заданный (непрнавильный) вопрос.