Created
February 7, 2020 21:57
-
-
Save mrzechonek/d05bc795417e7635f6e0cf5b535af216 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <stdbool.h> | |
#include <stddef.h> | |
#include <stdlib.h> | |
#include <string.h> | |
#include "gsmat.h" | |
#include "log.h" | |
#include "utils.h" | |
/* | |
* This a parser for Hayes command set used to talk with GSM modem. | |
* | |
* Parser works in a push mode, so modem driver needs to continuously read | |
* characters from modem's UART and feed them into the parser. | |
* | |
* Stream received from the modem can take three basic forms: a command response, | |
* an unsolicited notification and a command result/error code. | |
* | |
* Responses and notifications look pretty much the same (from the grammar | |
* perspective), while a result is a single line with 'OK', 'ERROR' etc. They | |
* are all reported to upper layer using one of the callbacks provided | |
* in gsmat_init() function. | |
* | |
* Because of this grammar ambiguity, that parser alone cannot distinguish | |
* between command response and unsolicited notification, and the modem driver | |
* needs a state machine to determine whether we issued a command and are | |
* waiting for response, or didn't, and are waiting for a notification. | |
* | |
* What's worse, ZTE's MF206A modem sometimes sends notifications while the | |
* command is being issued (there is no internal "guard time" to postpone | |
* notifications when a command is being read), so it may happen that such a | |
* notification is mistaken for a command response. There is no way to work | |
* around it on a driver/parser level, and the application code needs to take | |
* this possibility into account by checking response content and retrying | |
* commands. | |
* | |
* Also, because we use modem's internal TCP/IP stack, there are a few *very* | |
* long commands and notifications (notably +ZIPSEND and +ZIPRECV). Because we | |
* don't have enough RAM to buffer them as regular responses, their content is | |
* passed to application layer on the fly, using recv_callback(). See _element | |
* action for details. | |
*/ | |
#ifdef CONFIG_LOG_GSMAT | |
#undef LOCAL_LOG_LEVEL | |
#define LOCAL_LOG_LEVEL GLOBAL_LOG_LEVEL | |
#undef LOCAL_LOG_PREFIX | |
#define LOCAL_LOG_PREFIX "GSMAT:" | |
#endif | |
struct gsmat_cb | |
{ | |
/* | |
* Ragel's state | |
*/ | |
int cs; | |
/* | |
* buffer for "+COMMAND" string, used both in responses and unsolicited | |
* notifications | |
*/ | |
char *command; | |
char command_buf[GSMAT_COMMAND_SIZE]; | |
/* buffer for "OK", "ERROR" etc. */ | |
char *result; | |
char result_buf[GSMAT_RESULT_SIZE]; | |
/* | |
* buffer for response/notification arguments | |
* this is a list of null-delimited strings, ends with two consecutive | |
* nulls | |
*/ | |
char *argument; | |
char argument_buf[GSMAT_ARGUMENT_SIZE]; | |
/* | |
* internal buffer for a single argument | |
* this gets copied into argument_buf | |
*/ | |
char *element; | |
char element_buf[GSMAT_ELEMENT_SIZE]; | |
/* | |
* number of arguments accumulated so far | |
*/ | |
int argument_count; | |
/* | |
* called for each response or notification | |
*/ | |
void (*response_callback)(const char *, const char *); | |
/* | |
* called for each result | |
*/ | |
void (*result_callback)(const char *); | |
/* | |
* called for each byte during +ZIPRECV notification | |
*/ | |
void (*recv_callback)(uint8_t); | |
uint16_t recv_size; | |
uint16_t recv_count; | |
}; | |
static struct gsmat_cb gsmat_cb; | |
%%{ | |
machine gsmat; | |
access gsmat_cb.; | |
action signal_response | |
{ | |
/* | |
* Most responses are of "+COMMAND: <arguments>" form, but there are a | |
* few reported without the "+COMMAND" prefix. In such cases we report | |
* only the arguments. | |
*/ | |
if (gsmat_cb.command != gsmat_cb.command_buf || gsmat_cb.argument != gsmat_cb.argument_buf) | |
{ | |
if ((strcmp_P(gsmat_cb.command_buf, PSTR("+ZIPRECV")) == 0) && | |
(gsmat_cb.recv_count != gsmat_cb.recv_size * 2)) | |
{ | |
_ERR("ziprecv:%"PRIu16":%"PRIu16"", gsmat_cb.recv_count, gsmat_cb.recv_size * 2); | |
} | |
if (gsmat_cb.response_callback) | |
{ | |
gsmat_cb.response_callback(gsmat_cb.command_buf, gsmat_cb.argument_buf); | |
} | |
} | |
gsmat_cb.command = gsmat_cb.command_buf; | |
memset(gsmat_cb.command_buf, 0, sizeof(gsmat_cb.command_buf)); | |
gsmat_cb.argument = gsmat_cb.argument_buf; | |
memset(gsmat_cb.argument_buf, 0, sizeof(gsmat_cb.argument_buf)); | |
gsmat_cb.element = gsmat_cb.element_buf; | |
memset(gsmat_cb.element_buf, 0, sizeof(gsmat_cb.element_buf)); | |
gsmat_cb.argument_count = 0; | |
} | |
action signal_result | |
{ | |
if (gsmat_cb.result != gsmat_cb.result_buf) | |
{ | |
if (gsmat_cb.result_callback) | |
{ | |
gsmat_cb.result_callback(gsmat_cb.result_buf); | |
} | |
} | |
gsmat_cb.result = gsmat_cb.result_buf; | |
memset(gsmat_cb.result_buf, 0, sizeof(gsmat_cb.result_buf)); | |
gsmat_cb.argument = gsmat_cb.argument_buf; | |
memset(gsmat_cb.argument_buf, 0, sizeof(gsmat_cb.argument_buf)); | |
gsmat_cb.element = gsmat_cb.element_buf; | |
memset(gsmat_cb.element_buf, 0, sizeof(gsmat_cb.element_buf)); | |
gsmat_cb.argument_count = 0; | |
} | |
action _command | |
{ | |
if (gsmat_cb.command < gsmat_cb.command_buf + sizeof(gsmat_cb.command_buf)) | |
{ | |
*gsmat_cb.command++ = fc; | |
*gsmat_cb.command = '\0'; | |
} | |
} | |
action _element | |
{ | |
static int8_t shift; | |
static uint8_t c; | |
/* | |
* +ZIPRECV's last argument can be very long, so we need to parse | |
* it on the fly | |
*/ | |
if (strcmp_P(gsmat_cb.command_buf, PSTR("+ZIPRECV")) == 0) | |
{ | |
switch (gsmat_cb.argument_count) | |
{ | |
case 0: | |
/* | |
* first argument resets state | |
*/ | |
c = 0; | |
shift = 1; | |
gsmat_cb.recv_size = 0; | |
gsmat_cb.recv_count = 0; | |
/* fall-through */ | |
default: | |
/* | |
* first 3 arguments are processed as usual | |
*/ | |
if (gsmat_cb.element < gsmat_cb.element_buf + sizeof(gsmat_cb.element_buf)) | |
{ | |
*gsmat_cb.element++ = fc; | |
*gsmat_cb.element = '\0'; | |
} | |
break; | |
case 3: | |
/* | |
* 4th argument denotes data length, in bytes | |
*/ | |
gsmat_cb.recv_size *= 10; | |
gsmat_cb.recv_size += fc - '0'; | |
break; | |
case 4: | |
/* | |
* when reading 5th argument, convert each 2 consecutive | |
* hex digits into a number and pass it to the application | |
*/ | |
c |= hex2num(fc) << (shift * 4); | |
gsmat_cb.recv_count++; | |
if (shift-- == 0) | |
{ | |
if (gsmat_cb.recv_callback) | |
{ | |
gsmat_cb.recv_callback(c); | |
} | |
c = 0; | |
shift = 1; | |
} | |
break; | |
} | |
} | |
else | |
{ | |
if (gsmat_cb.element < gsmat_cb.element_buf + sizeof(gsmat_cb.element_buf)) | |
{ | |
*gsmat_cb.element++ = fc; | |
*gsmat_cb.element = '\0'; | |
} | |
} | |
} | |
action _argument | |
{ | |
const size_t element_size = gsmat_cb.element - gsmat_cb.element_buf; | |
/* | |
* append current element to argument list | |
*/ | |
if (element_size && (gsmat_cb.argument + element_size < gsmat_cb.argument_buf + sizeof(gsmat_cb.argument_buf))) | |
{ | |
memcpy(gsmat_cb.argument, gsmat_cb.element_buf, element_size); | |
gsmat_cb.argument += element_size; | |
/* | |
* make sure the argument list is double-terminated | |
*/ | |
*gsmat_cb.argument++ = '\0'; | |
*gsmat_cb.argument = '\0'; | |
} | |
gsmat_cb.argument_count++; | |
gsmat_cb.element = gsmat_cb.element_buf; | |
memset(gsmat_cb.element_buf, 0, sizeof(gsmat_cb.element_buf)); | |
} | |
action _result | |
{ | |
if (gsmat_cb.result < gsmat_cb.result_buf + sizeof(gsmat_cb.result_buf)) | |
{ | |
*gsmat_cb.result++ = fc; | |
*gsmat_cb.result = '\0'; | |
} | |
} | |
crlf = '\r' '\n'; | |
# quoted string can contain characters with backslashes | |
quoted = '"' (/\\./ | [^"\\])* '"'; | |
range = '(' digit+ '-' digit+ ')'; | |
ip_address = ( digit+ '.' digit+ '.' digit+ '.' digit+ ); | |
list_element = ( (upper | digit)+ | quoted | range | ip_address) $_element %_argument; | |
list_delimiter = ( ',' space? ); | |
list = list_element ( list_delimiter list_element )*; | |
result = | |
( 'OK' | 'ERROR' ) $_result; | |
prefix = '+'; | |
# response is either a "+COMMAND: <arguments>" or just "<arguments>", but | |
# second form cannot start with "+COMMAND" and cannot overlap with one of | |
# result strings | |
response = ( | |
( prefix ( upper | digit )+ ) $_command ( ':' space list )? | | |
( ( ^prefix print+ ) - result ) $_element %_argument | |
); | |
command_response = ( | |
( response crlf )? @signal_response | | |
( result crlf )? @signal_result | |
); | |
main := ( 'ATE0\r' )? ( crlf command_response )*; | |
}%% | |
%%write data; | |
void gsmat_init(void (*response_callback)(const char *, const char *), | |
void (*result_callback)(const char *), | |
void (*recv_callback)(uint8_t)) | |
{ | |
%%write init; | |
gsmat_cb.command = gsmat_cb.command_buf; | |
memset(gsmat_cb.command_buf, 0, sizeof(gsmat_cb.command_buf)); | |
gsmat_cb.result = gsmat_cb.result_buf; | |
memset(gsmat_cb.result_buf, 0, sizeof(gsmat_cb.result_buf)); | |
gsmat_cb.argument = gsmat_cb.argument_buf; | |
memset(gsmat_cb.argument_buf, 0, sizeof(gsmat_cb.argument_buf)); | |
gsmat_cb.element = gsmat_cb.element_buf; | |
memset(gsmat_cb.element_buf, 0, sizeof(gsmat_cb.element_buf)); | |
gsmat_cb.argument_count = 0; | |
if (response_callback) | |
{ | |
gsmat_cb.response_callback = response_callback; | |
} | |
if (result_callback) | |
{ | |
gsmat_cb.result_callback = result_callback; | |
} | |
if (recv_callback) | |
{ | |
gsmat_cb.recv_callback = recv_callback; | |
} | |
} | |
bool gsmat_write(const char *p_buffer, size_t p_size) | |
{ | |
const char *p = p_buffer; | |
const char *pe = p + p_size; | |
const char *eof = pe; | |
(void)eof; | |
while(p != pe) | |
{ | |
%%write exec; | |
if(gsmat_cb.cs == %%{ write error; }%%) | |
{ | |
_ERR("parse: \\x%02hhx (%c)", *p_buffer, *p_buffer); | |
return false; | |
} | |
} | |
return true; | |
} | |
/* | |
* vim:ft=ragel | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment