ESP8266-based WiFi serial modem emulator ROM
1/*
2 * WiFiPPP
3 * Copyright (c) 2021 joshua stein <jcs@jcs.org>
4 *
5 * Permission to use, copy, modify, and distribute this software for any
6 * purpose with or without fee is hereby granted, provided that the above
7 * copyright notice and this permission notice appear in all copies.
8 *
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16 */
17
18/*
19 * Useful AT command sets to emulate:
20 *
21 * USRobotics Courier 56K Business Modem
22 * http://web.archive.org/web/20161116174421/http://support.usr.com/support/3453c/3453c-ug/alphabetic.html
23 *
24 * Xecom XE3314L
25 * http://web.archive.org/web/20210816224031/http://static6.arrow.com/aropdfconversion/63e466a4e0c7e004c40f79e4dbe4c1356a3dcef6/xe3314l.pdf
26 */
27
28#include "wifippp.h"
29
30uint8_t state = STATE_AT;
31
32static char curcmd[64] = { 0 };
33static char lastcmd[64] = { 0 };
34static unsigned char curcmdlen = 0;
35static unsigned char lastcmdlen = 0;
36static unsigned char plusses = 0;
37static unsigned long plus_wait = 0;
38static unsigned long last_dtr = 0;
39static unsigned long last_autobaud = 0;
40static unsigned long last_pixel_color = 0;
41
42void
43loop(void)
44{
45 int b = -1, i;
46 long now = millis();
47 bool hangup = false;
48
49 if (last_pixel_color - now > 500) {
50 pixel_color_by_state();
51 last_pixel_color = now;
52 }
53
54 socks_process();
55
56 if (serial_dtr()) {
57 if (!last_dtr) {
58 if (settings->autobaud) {
59#ifdef AT_TRACE
60 syslog.logf(LOG_DEBUG, "new connection with "
61 "DTR, doing auto-baud");
62#endif
63 serial_autobaud();
64 now = millis();
65 last_autobaud = now;
66 } else
67 serial_start(settings->baud);
68 }
69 last_dtr = now;
70 } else if (last_dtr && (now - last_dtr > 1750)) {
71 /* had DTR, dropped it for 1.75 secs, hangup */
72 hangup = true;
73 last_dtr = 0;
74 syslog.log(LOG_DEBUG, "dropped DTR, hanging up");
75 }
76
77 switch (state) {
78 case STATE_AT:
79 if (!(serial_available() && (b = serial_read())))
80 return;
81
82 /* USR modem mode, ignore input not starting with at or a/ */
83 if (curcmdlen == 0 && (b != 'A' && b != 'a')) {
84#ifdef AT_TRACE
85 syslog.logf(LOG_DEBUG, "bogus char: 0x%x (%c)", b, b);
86#endif
87 if (b > 127) {
88 /*
89 * Help modem auto-detection by just spitting
90 * out OK in response to hopefully ATs received
91 * during autobaud
92 */
93 if (!settings->quiet) {
94 if (settings->verbal)
95 output("\r\nOK\r\n");
96 else
97 output("\r0\r");
98 }
99 }
100 return;
101 }
102
103 if (curcmdlen == 1 && b == '/') {
104 if (settings->echo)
105 output("/\r");
106 curcmd[0] = '\0';
107 curcmdlen = 0;
108 exec_cmd((char *)&lastcmd, lastcmdlen);
109 break;
110 } else if (curcmdlen == 1 && (b != 'T' && b != 't')) {
111 if (settings->echo)
112 output("\b \b");
113 curcmdlen = 0;
114 return;
115 }
116
117 switch (b) {
118 case '\n':
119 case '\r':
120 if (b == '\r') {
121 /* if sender is using \r\n, ignore the \n */
122 now = millis();
123 while (millis() - now < 10) {
124 int b2 = serial_peek();
125 if (b2 == -1)
126 continue;
127 else if (b2 == '\n') {
128 /* this is a \r\n, ignore \n */
129 serial_read();
130 break;
131 } else {
132 /* some other data */
133 break;
134 }
135 }
136 }
137 output("\r");
138 curcmd[curcmdlen] = '\0';
139 exec_cmd((char *)&curcmd, curcmdlen);
140 curcmd[0] = '\0';
141 curcmdlen = 0;
142 break;
143 case '\b':
144 case 127:
145 if (curcmdlen) {
146 if (settings->echo)
147 output("\b \b");
148 curcmdlen--;
149 }
150 break;
151 default:
152 curcmd[curcmdlen++] = b;
153 if (settings->echo)
154 output(b);
155 }
156 break;
157 case STATE_TELNET:
158 b = -1;
159
160 if (hangup) {
161 telnet_disconnect();
162 break;
163 }
164
165 if (serial_available())
166 b = serial_read();
167
168 if (b == -1 && plus_wait > 0 && (millis() - plus_wait) >= 500) {
169 /* received no input within 500ms of a plus */
170 if (plusses >= 3) {
171 state = STATE_AT;
172 if (!settings->quiet) {
173 if (settings->verbal)
174 output("\r\nOK\r\n");
175 else
176 output("0\r");
177 }
178 } else {
179 /* cancel, flush any plus signs received */
180 for (i = 0; i < plusses; i++)
181 telnet_write("+");
182 }
183 plusses = 0;
184 plus_wait = 0;
185 } else if (b != -1) {
186 if (b == '+') {
187 plusses++;
188 plus_wait = millis();
189 break;
190 }
191
192 if (plusses) {
193 for (i = 0; i < plusses; i++)
194 telnet_write("+");
195 plusses = 0;
196 }
197 plus_wait = 0;
198 telnet_write(b);
199 break;
200 }
201
202 if ((b = telnet_read()) != -1) {
203 serial_write(b);
204 return;
205 } else if (!telnet_connected()) {
206 if (!settings->quiet) {
207 if (settings->verbal)
208 output("\r\nNO CARRIER\r\n");
209 else
210 output("3\r");
211 }
212 state = STATE_AT;
213 break;
214 }
215 break;
216 case STATE_PPP:
217 if (hangup) {
218 ppp_stop(true);
219 break;
220 }
221
222 ppp_process();
223 break;
224 }
225}
226
227void
228exec_cmd(char *cmd, size_t len)
229{
230 char *errstr = NULL;
231 char *lcmd, *olcmd;
232 char cmd_char;
233 uint8_t cmd_num = 0;
234 bool did_nl = false;
235 bool did_response = false;
236
237 lcmd = olcmd = (char *)malloc(len + 1);
238 if (lcmd == NULL) {
239 if (settings->verbal)
240 outputf("ERROR malloc %zu failed\r\n", len);
241 else
242 output("4\r");
243 return;
244 }
245
246#ifdef AT_TRACE
247 syslog.logf(LOG_DEBUG, "%s: parsing \"%s\"", __func__, cmd);
248#endif
249
250 for (size_t i = 0; i < len; i++)
251 lcmd[i] = tolower(cmd[i]);
252 lcmd[len] = '\0';
253
254 /* shouldn't be able to get here, but just in case */
255 if (len < 2 || lcmd[0] != 'a' || lcmd[1] != 't') {
256 errstr = strdup("not an AT command");
257 goto error;
258 }
259
260 memcpy(&lastcmd, lcmd, len + 1);
261 lastcmdlen = len;
262
263 /* strip AT */
264 cmd += 2;
265 lcmd += 2;
266 len -= 2;
267
268parse_cmd:
269 if (lcmd[0] == '\0')
270 goto done_parsing;
271
272 /* remove command character */
273 cmd_char = lcmd[0];
274 len--;
275 cmd++;
276 lcmd++;
277
278 /* find optional single digit after command, defaulting to 0 */
279 cmd_num = 0;
280 if (cmd[0] >= '0' && cmd[0] <= '9') {
281 if (cmd[1] >= '0' && cmd[1] <= '9')
282 /* nothing uses more than 1 digit */
283 goto error;
284 cmd_num = cmd[0] - '0';
285 len--;
286 cmd++;
287 lcmd++;
288 }
289
290#ifdef AT_TRACE
291 syslog.logf(LOG_DEBUG, "%s: parsing AT %c[%d] args \"%s\"", __func__,
292 cmd_char, cmd_num, lcmd);
293#endif
294
295 switch (cmd_char) {
296 case 'd': {
297 char *host, *ohost, *bookmark;
298 uint16_t port;
299 int chars;
300 int index;
301
302 if (len < 2)
303 goto error;
304
305 switch (lcmd[0]) {
306 case 't':
307 /* ATDT: dial a host */
308 host = ohost = (char *)malloc(len);
309 if (host == NULL)
310 goto error;
311 host[0] = '\0';
312 if (sscanf(lcmd, "t%[^:]:%hu%n", host, &port,
313 &chars) == 2 && chars > 0)
314 /* matched host:port */
315 ;
316 else if (sscanf(lcmd, "t%[^:]%n", host, &chars) == 1
317 && chars > 0)
318 /* host without port */
319 port = 23;
320 else {
321 errstr = strdup("invalid hostname");
322 goto error;
323 }
324 break;
325 case 's':
326 /* ATDS: dial a stored host */
327 if (sscanf(lcmd, "s%d", &index) != 1)
328 goto error;
329
330 if (index < 1 || index > NUM_BOOKMARKS) {
331 errstr = strdup("invalid index");
332 goto error;
333 }
334
335 bookmark = settings->bookmarks[index - 1];
336
337 host = ohost = (char *)malloc(BOOKMARK_SIZE);
338 if (host == NULL)
339 goto error;
340
341 host[0] = '\0';
342
343 if (sscanf(bookmark, "%[^:]:%hu%n", host, &port,
344 &chars) == 2 && chars > 0)
345 /* matched host:port */
346 ;
347 else if (sscanf(bookmark, "%[^:]%n", host, &chars) == 1
348 && chars > 0)
349 /* host without port */
350 port = 23;
351 else {
352 errstr = strdup("invalid hostname");
353 goto error;
354 }
355 break;
356 case 'p':
357 if (strncmp(lcmd, "ppp", 3) == 0) {
358 /*
359 * We can't easily support ATD because we allow
360 * hostnames, so if the user typed
361 * "atdtelnethost", we can't tell whether they
362 * meant "atd telnethost" or "atdt elnethost",
363 * but we can allow PPP since that is easy to
364 * check and will probably be done
365 * automatically by PPP software.
366 */
367 host = ohost = (char *)malloc(4);
368 if (host == NULL)
369 goto error;
370 sprintf(host, "ppp");
371 } else
372 goto error;
373 break;
374 default:
375 goto error;
376 }
377
378 /* skip leading spaces */
379 while (host[0] == ' ')
380 host++;
381
382 if (host[0] == '\0') {
383 errstr = strdup("blank hostname");
384 goto error;
385 }
386
387 /* no commands can follow */
388 len = 0;
389
390 if (strcasecmp(host, "ppp") == 0) {
391 ip4_addr_t t_addr;
392 ip_addr_copy(t_addr, settings->ppp_server_ip);
393 if (!settings->quiet) {
394 if (settings->verbal)
395 outputf("\nDIALING %s:PPP\r\n",
396 ipaddr_ntoa(&t_addr));
397 }
398
399 telnet_disconnect();
400 if (ppp_start())
401 /* ppp_begin outputs CONNECT line, since it has
402 * to do so before calling ppp_listen */
403 state = STATE_PPP;
404 else if (!settings->quiet) {
405 if (settings->verbal)
406 output("NO ANSWER\r\n");
407 else
408 output("8\r");
409 }
410 } else {
411 if (!settings->quiet && settings->verbal)
412 outputf("\nDIALING %s:%d\r\n", host, port);
413
414 if (telnet_connect(host, port) == 0) {
415 if (!settings->quiet) {
416 if (settings->verbal)
417 outputf("CONNECT %d %s:%d\r\n",
418 Serial.baudRate(), host,
419 port);
420 else
421 output("18\r"); /* 57600 */
422 }
423 state = STATE_TELNET;
424 } else if (!settings->quiet) {
425 if (settings->verbal)
426 output("\nNO ANSWER\r\n");
427 else
428 output("8\r");
429 }
430 }
431
432 if (!settings->quiet)
433 did_nl = true;
434
435 free(ohost);
436 break;
437 }
438 case 'e':
439 /* ATE/ATE0 or ATE1: disable or enable echo */
440 switch (cmd_num) {
441 case 0:
442 settings->echo = 0;
443 break;
444 case 1:
445 settings->echo = 1;
446 break;
447 default:
448 goto error;
449 }
450 break;
451 case 'h':
452 /* ATH/ATH0: hangup */
453 switch (cmd_num) {
454 case 0:
455 telnet_disconnect();
456 output("\r\n");
457 break;
458 default:
459 goto error;
460 }
461 break;
462 case 'i':
463 /* ATI/ATI#: show information pages */
464 switch (cmd_num) {
465 case 0:
466 case 3:
467 /* ATI/ATI0/ATI3: show product name */
468 outputf("\njcs WiFiPPP %s\r\n", WIFIPPP_VERSION);
469 did_nl = true;
470 break;
471 case 1:
472 /* ATI1: checksum of RAM (not used) */
473 output("\n1337\r\n");
474 did_nl = true;
475 break;
476 case 2:
477 /* ATI2: test RAM (show ram info) */
478 outputf("\nFree heap: %d\r\n", ESP.getFreeHeap());
479 did_nl = true;
480 break;
481 case 4: {
482 /* ATI4: show settings */
483 ip4_addr_t t_addr;
484
485 output("\n");
486
487 outputf("Firmware version: %s\r\n", WIFIPPP_VERSION);
488
489 outputf("Default baud rate: %d\r\n", settings->baud);
490 outputf("Current baud rate: %d\r\n",
491 Serial.baudRate());
492
493 outputf("Default WiFi SSID: %s\r\n",
494 settings->wifi_ssid);
495 outputf("Current WiFi SSID: %s\r\n", WiFi.SSID());
496 outputf("WiFi connected: %s\r\n",
497 WiFi.status() == WL_CONNECTED ? "yes" : "no");
498 if (WiFi.status() == WL_CONNECTED) {
499 outputf("IP address: %s\r\n",
500 WiFi.localIP().toString().c_str());
501 outputf("Gateway IP: %s\r\n",
502 WiFi.gatewayIP().toString().c_str());
503 outputf("DNS server IP: %s\r\n",
504 WiFi.dnsIP().toString().c_str());
505 }
506
507 ip_addr_copy(t_addr, settings->ppp_server_ip);
508 outputf("PPP server: %s\r\n",
509 ipaddr_ntoa(&t_addr));
510 ip_addr_copy(t_addr, settings->ppp_client_ip);
511 outputf("PPP client: %s\r\n",
512 ipaddr_ntoa(&t_addr));
513
514 outputf("Syslog server: %s\r\n",
515 settings->syslog_server);
516
517 for (int i = 0; i < NUM_BOOKMARKS; i++) {
518 if (settings->bookmarks[i][0] != '\0')
519 outputf("ATDS bookmark %d: %s\r\n",
520 i + 1, settings->bookmarks[i]);
521 }
522
523 did_nl = true;
524 break;
525 }
526 case 5: {
527 /* ATI5: scan for wifi networks */
528 int n = WiFi.scanNetworks();
529
530 output("\n");
531
532 for (int i = 0; i < n; i++) {
533 outputf("%02d: %s (chan %d, %ddBm, ",
534 i + 1,
535 WiFi.SSID(i).c_str(),
536 WiFi.channel(i),
537 WiFi.RSSI(i));
538
539 switch (WiFi.encryptionType(i)) {
540 case ENC_TYPE_WEP:
541 output("WEP");
542 break;
543 case ENC_TYPE_TKIP:
544 output("WPA-PSK");
545 break;
546 case ENC_TYPE_CCMP:
547 output("WPA2-PSK");
548 break;
549 case ENC_TYPE_NONE:
550 output("NONE");
551 break;
552 case ENC_TYPE_AUTO:
553 output("WPA-PSK/WPA2-PSK");
554 break;
555 default:
556 outputf("?(%d)",
557 WiFi.encryptionType(i));
558 }
559
560 output(")\r\n");
561 }
562 did_nl = true;
563 break;
564 }
565 default:
566 goto error;
567 }
568 break;
569 case 'o':
570 /* ATO: go back online after a +++ */
571 switch (cmd_num) {
572 case 0:
573 if (telnet_connected())
574 state = STATE_TELNET;
575 else
576 goto error;
577 break;
578 default:
579 goto error;
580 }
581 break;
582 case 'q':
583 /* ATQ/ATQ0 or ATQ1: enable or disable quiet */
584 switch (cmd_num) {
585 case 0:
586 settings->quiet = 0;
587 break;
588 case 1:
589 case 2:
590 settings->quiet = 1;
591 break;
592 default:
593 goto error;
594 }
595 break;
596 case 'v':
597 /* ATV/ATV0 or ATV1: enable or disable verbal responses */
598 switch (cmd_num) {
599 case 0:
600 settings->verbal = 0;
601 break;
602 case 1:
603 settings->verbal = 1;
604 break;
605 default:
606 goto error;
607 }
608 break;
609 case 'x':
610 /* ATX/ATX#: ignore dialtone, certain results (not used) */
611 break;
612 case 'z':
613 /* ATZ/ATZ0: restart */
614 switch (cmd_num) {
615 case 0:
616 if (!settings->quiet) {
617 if (settings->verbal)
618 output("\nOK\r\n");
619 else
620 output("0\r");
621 }
622 ESP.restart();
623 /* NOTREACHED */
624 default:
625 goto error;
626 }
627 break;
628 case '$':
629 /* wifi232 commands, all consume the rest of the input string */
630 if (strcmp(lcmd, "autobaud=0") == 0) {
631 /* AT$AUTOBAUD=0: disable autobaud setting */
632 settings->autobaud = 0;
633 } else if (strcmp(lcmd, "autobaud=1") == 0) {
634 /* AT$AUTOBAUD=1: enable autobaud setting */
635 settings->autobaud = 1;
636 } else if (strcmp(lcmd, "autobaud?") == 0) {
637 /* AT$AUTOBAUD?: print autobaud setting */
638 outputf("\n%d\r\n", settings->autobaud);
639 did_nl = true;
640 } else if (strncmp(lcmd, "baud=", 5) == 0) {
641 uint32_t baud = 0;
642 int i, chars = 0;
643
644 /* AT$BAUD=...: set baud rate */
645 if (sscanf(lcmd, "baud=%d%n", &baud, &chars) != 1 ||
646 chars == 0) {
647 errstr = strdup("invalid baud rate");
648 goto error;
649 }
650
651 for (i = 0; ; i++) {
652 if (ok_bauds[i] == 0) {
653 errstr = strdup("unsupported baud "
654 "rate");
655 goto error;
656 }
657 if (ok_bauds[i] == baud)
658 break;
659 }
660
661 settings->baud = baud;
662 if (!settings->quiet) {
663 if (settings->verbal)
664 outputf("\nOK switching to %d\r\n",
665 settings->baud);
666 else
667 output("0\r");
668 }
669 serial_flush();
670 serial_start(settings->baud);
671 } else if (strcmp(lcmd, "baud?") == 0) {
672 /* AT$BAUD?: print default baud rate */
673 outputf("\n%d\r\n", settings->baud);
674 did_nl = true;
675 } else if (strcmp(lcmd, "led?") == 0) {
676 /* AT$LED?: show pixel brightness setting */
677 outputf("\n%d\r\n", settings->pixel_brightness);
678 did_nl = true;
679 } else if (strncmp(lcmd, "led=", 4) == 0) {
680 /* AT$LED=n: set pixel brightness */
681 int br, chars;
682 if (sscanf(lcmd, "led=%d%n", &br, &chars) != 1 ||
683 chars == 0 || br < 0 || br > 10) {
684 errstr = strdup("brightness must be between "
685 "0 and 10");
686 goto error;
687 }
688 settings->pixel_brightness = br;
689 pixel_adjust_brightness();
690 } else if (strncmp(lcmd, "naws=", 5) == 0) {
691 /* AT$NAWS=: set telnet NAWS */
692 int w, h, chars;
693 if (sscanf(lcmd + 5, "%dx%d%n", &w, &h, &chars) == 2 &&
694 chars > 0) {
695 if (w < 1 || w > 255) {
696 errstr = strdup("invalid width");
697 goto error;
698 }
699 if (h < 1 || h > 255) {
700 errstr = strdup("invalid height");
701 goto error;
702 }
703
704 settings->telnet_tts_w = w;
705 settings->telnet_tts_h = h;
706 } else {
707 errstr = strdup("must be WxH");
708 goto error;
709 }
710 } else if (strcmp(lcmd, "naws?") == 0) {
711 /* AT$NAWS?: show telnet NAWS setting */
712 outputf("\n%dx%d\r\n", settings->telnet_tts_w,
713 settings->telnet_tts_h);
714 did_nl = true;
715 } else if (strcmp(lcmd, "net=0") == 0) {
716 /* AT$NET=0: disable telnet setting */
717 settings->telnet = 0;
718 } else if (strcmp(lcmd, "net=1") == 0) {
719 /* AT$NET=1: enable telnet setting */
720 settings->telnet = 1;
721 } else if (strcmp(lcmd, "net?") == 0) {
722 /* AT$NET?: show telnet setting */
723 outputf("\n%d\r\n", settings->telnet);
724 did_nl = true;
725 } else if (strncmp(lcmd, "pass=", 5) == 0) {
726 /* AT$PASS=...: store wep/wpa passphrase */
727 memset(settings->wifi_pass, 0,
728 sizeof(settings->wifi_pass));
729 strncpy(settings->wifi_pass, cmd + 5,
730 sizeof(settings->wifi_pass));
731
732 WiFi.disconnect();
733 if (settings->wifi_ssid[0])
734 WiFi.begin(settings->wifi_ssid,
735 settings->wifi_pass);
736 } else if (strcmp(lcmd, "pass?") == 0) {
737 /* AT$PASS?: print wep/wpa passphrase */
738 outputf("\n%s\r\n", settings->wifi_pass);
739 did_nl = true;
740 } else if (strncmp(lcmd, "pppc=", 5) == 0) {
741 /* AT$PPPC=...: store PPP client IP */
742 ip4_addr_t t_addr;
743
744 if (!ipaddr_aton(cmd + 5, &t_addr)) {
745 errstr = strdup("invalid IP");
746 goto error;
747 }
748
749 ip_addr_copy(settings->ppp_client_ip, t_addr);
750 } else if (strcmp(lcmd, "pppc?") == 0) {
751 /* AT$PPPC?: print PPP client IP */
752 ip4_addr_t t_addr;
753 ip_addr_copy(t_addr, settings->ppp_client_ip);
754 outputf("\n%s\r\n", ipaddr_ntoa(&t_addr));
755 did_nl = true;
756 } else if (strncmp(lcmd, "ppps=", 5) == 0) {
757 /* AT$PPPS=...: store PPP server IP */
758 ip4_addr_t t_addr;
759
760 if (!ipaddr_aton(cmd + 5, &t_addr)) {
761 errstr = strdup("invalid IP");
762 goto error;
763 }
764
765 ip_addr_copy(settings->ppp_server_ip, t_addr);
766 /* re-bind to the new ip */
767 socks_setup();
768 } else if (strcmp(lcmd, "ppps?") == 0) {
769 /* AT$PPPS?: print PPP server IP */
770 ip4_addr_t t_addr;
771 ip_addr_copy(t_addr, settings->ppp_server_ip);
772 outputf("\n%s\r\n", ipaddr_ntoa(&t_addr));
773 did_nl = true;
774 } else if (strncmp(lcmd, "ssid=", 5) == 0) {
775 /* AT$SSID=...: set wifi ssid */
776 memset(settings->wifi_ssid, 0,
777 sizeof(settings->wifi_ssid));
778 strncpy(settings->wifi_ssid, cmd + 5,
779 sizeof(settings->wifi_ssid));
780
781 WiFi.disconnect();
782 if (settings->wifi_ssid[0])
783 WiFi.begin(settings->wifi_ssid,
784 settings->wifi_pass);
785 } else if (strcmp(lcmd, "ssid?") == 0) {
786 /* AT$SSID?: print wifi ssid */
787 outputf("\n%s\r\n", settings->wifi_ssid);
788 did_nl = true;
789 } else if (strncmp(lcmd, "syslog=", 7) == 0) {
790 /* AT$SYSLOG=...: set syslog server */
791 memset(settings->syslog_server, 0,
792 sizeof(settings->syslog_server));
793 strncpy(settings->syslog_server, cmd + 7,
794 sizeof(settings->syslog_server));
795 syslog_setup();
796 syslog.logf(LOG_INFO, "syslog server changed to %s",
797 settings->syslog_server);
798 } else if (strcmp(lcmd, "syslog?") == 0) {
799 /* AT$SYSLOG?: print syslog server */
800 outputf("\n%s\r\n", settings->syslog_server);
801 did_nl = true;
802 } else if (strncmp(lcmd, "ttype=", 6) == 0) {
803 /* AT$TTYPE=: set telnet TTYPE */
804 memset(settings->telnet_tterm, 0,
805 sizeof(settings->telnet_tterm));
806 strncpy(settings->telnet_tterm, cmd + 4,
807 sizeof(settings->telnet_tterm));
808 } else if (strcmp(lcmd, "ttype?") == 0) {
809 /* AT$TTYPE?: show telnet TTYPE setting */
810 outputf("\n%s\r\n", settings->telnet_tterm);
811 did_nl = true;
812 } else if (strncmp(lcmd, "update?", 7) == 0) {
813 /* AT$UPDATE?: show whether an OTA update is available */
814 char *url = NULL;
815 if (strncmp(lcmd, "update? http", 12) == 0)
816 url = lcmd + 8;
817 update_process(url, false, false);
818 did_response = true;
819 } else if (strncmp(lcmd, "update!", 7) == 0) {
820 /* AT$UPDATE!: force an OTA update */
821 char *url = NULL;
822 if (strncmp(lcmd, "update! http", 12) == 0)
823 url = lcmd + 8;
824 update_process(url, true, true);
825 did_response = true;
826 } else if (strcmp(lcmd, "update") == 0 ||
827 strncmp(lcmd, "update http", 11) == 0) {
828 /* AT$UPDATE: do an OTA update */
829 char *url = NULL;
830 if (strncmp(lcmd, "update http", 11) == 0)
831 url = lcmd + 7;
832 update_process(url, true, false);
833 did_response = true;
834 } else
835 goto error;
836
837 /* consume all chars */
838 len = 0;
839 break;
840 case '&':
841 if (cmd[0] == '\0')
842 goto error;
843
844 cmd_char = lcmd[0];
845 len--;
846 cmd++;
847 lcmd++;
848
849 /* find optional single digit after &command, defaulting to 0 */
850 cmd_num = 0;
851 if (cmd[0] == '?') {
852 cmd_num = '?';
853 len--;
854 cmd++;
855 lcmd++;
856 } else if (cmd[0] >= '0' && cmd[0] <= '9') {
857 if (cmd[1] >= '0' && cmd[1] <= '9')
858 /* nothing uses more than 1 digit */
859 goto error;
860 cmd_num = cmd[0] - '0';
861 len--;
862 cmd++;
863 lcmd++;
864 }
865
866 switch (cmd_char) {
867 case 'w':
868 switch (cmd_num) {
869 case 0:
870 /* AT&W: save settings */
871 /* force setting dirty */
872 (void)EEPROM.getDataPtr();
873 if (!EEPROM.commit())
874 goto error;
875 break;
876 default:
877 goto error;
878 }
879 break;
880 case 'z': {
881 /* AT&Z: manage bookmarks */
882 uint32_t index = 0;
883 uint8_t query;
884 int chars = 0;
885
886 if (sscanf(lcmd, "%u=%n", &index, &chars) == 1 &&
887 chars > 0) {
888 /* AT&Zn=...: store address */
889 query = 0;
890 } else if (sscanf(lcmd, "%u?%n", &index, &chars) == 1 &&
891 chars > 0) {
892 /* AT&Zn?: query stored address */
893 query = 1;
894 } else {
895 errstr = strdup("invalid store command");
896 goto error;
897 }
898
899 if (index < 1 || index > NUM_BOOKMARKS) {
900 errstr = strdup("invalid index");
901 goto error;
902 }
903
904 if (query) {
905 outputf("\n%s\r\n",
906 settings->bookmarks[index - 1]);
907 did_nl = true;
908 } else {
909 memset(settings->bookmarks[index - 1], 0,
910 sizeof(settings->bookmarks[0]));
911 strncpy(settings->bookmarks[index - 1],
912 cmd + 2,
913 sizeof(settings->bookmarks[0]) - 1);
914 }
915
916 /* consume all chars */
917 len = 0;
918 break;
919 }
920 default:
921 goto error;
922 }
923 break;
924 case ' ':
925 /* skip spaces between commands */
926 break;
927 default:
928 goto error;
929 }
930
931done_parsing:
932 /* if any len left, parse as another command */
933 if (len > 0)
934 goto parse_cmd;
935
936 if (olcmd)
937 free(olcmd);
938
939 if (!did_response && state == STATE_AT && !settings->quiet) {
940 if (settings->verbal)
941 outputf("%sOK\r\n", did_nl ? "" : "\n");
942 else
943 output("0\r");
944 }
945
946 return;
947
948error:
949 if (olcmd)
950 free(olcmd);
951
952 if (!did_response && !settings->quiet) {
953 if (settings->verbal) {
954 output("\nERROR");
955 if (errstr != NULL)
956 outputf(" %s", errstr);
957 output("\r\n");
958 } else
959 output("4\r");
960 }
961
962 if (errstr != NULL)
963 free(errstr);
964}