2 * ESP32 Scan / Factory protocol handler
4 * Copyright (C) 2017 Andy Green <andy@warmcat.com>
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation:
9 * version 2.1 of the License.
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
24 #include <esp_ota_ops.h>
29 SCAN_STATE_INITIAL_MANIFEST,
39 struct per_session_data__esplws_scan {
40 struct per_session_data__esplws_scan *next;
41 scan_state scan_state;
42 struct timeval last_send;
46 char result[LWS_PRE + 512];
47 unsigned char buffer[4096];
54 unsigned char subsequent:1;
55 unsigned char changed_partway:1;
58 struct per_vhost_data__esplws_scan {
59 wifi_ap_record_t ap_records[10];
60 TimerHandle_t timer, reboot_timer;
61 struct per_session_data__esplws_scan *live_pss_list;
62 struct lws_context *context;
63 struct lws_vhost *vhost;
64 const struct lws_protocols *protocol;
66 const esp_partition_t *part;
67 esp_ota_handle_t otahandle;
75 uint16_t count_ap_records;
77 unsigned char scan_ongoing:1;
78 unsigned char completed_any_scan:1;
79 unsigned char reboot:1;
80 unsigned char changed_settings:1;
81 unsigned char checked_updates:1;
82 unsigned char autonomous_update:1;
83 unsigned char autonomous_update_sampled:1;
86 static const struct store_json store_json[] = {
87 { "ssid\":\"", "ssid" },
88 { ",\"pw\":\"", "password" },
89 { ",\"access_pw\":\"", "access_pw" },
90 { ",\"region\":\"", "region" },
93 static wifi_scan_config_t scan_config = {
100 extern void (*lws_cb_scan_done)(void *);
101 extern void *lws_cb_scan_done_arg;
103 const esp_partition_t *
104 ota_choose_part(void);
106 static const char * const param_names[] = {
114 enum enum_param_names {
124 scan_finished(void *v);
127 esplws_simple_arg(char *dest, int len, const char *in, const char *match)
129 const char *p = strstr(in, match);
136 while (*p && *p != '\"' && n < len - 1)
144 scan_start(struct per_vhost_data__esplws_scan *vhd)
151 if (vhd->scan_ongoing)
154 vhd->scan_ongoing = 1;
155 lws_cb_scan_done = scan_finished;
156 lws_cb_scan_done_arg = vhd;
157 n = esp_wifi_scan_start(&scan_config, false);
159 lwsl_err("scan start failed %d\n", n);
162 static void timer_cb(TimerHandle_t t)
164 struct per_vhost_data__esplws_scan *vhd = pvTimerGetTimerID(t);
169 static void reboot_timer_cb(TimerHandle_t t)
175 client_connection(struct per_vhost_data__esplws_scan *vhd, const char *file)
177 #if CONFIG_LWS_IS_FACTORY_APPLICATION == 'y' && defined(CONFIG_LWS_OTA_SERVER_BASE_URL) && \
178 defined(CONFIG_LWS_OTA_SERVER_FQDN)
179 static struct lws_client_connect_info i;
182 memset(&i, 0, sizeof i);
184 snprintf(path, sizeof(path) - 1, CONFIG_LWS_OTA_SERVER_BASE_URL "/" CONFIG_LWS_MODEL_NAME "/%s", file);
186 lwsl_notice("Fetching %s\n", path);
189 i.context = vhd->context;
190 i.address = CONFIG_LWS_OTA_SERVER_FQDN;
191 i.ssl_connection = 1;
194 i.vhost = vhd->vhost;
197 i.protocol = "esplws-scan";
200 vhd->cwsi = lws_client_connect_via_info(&i);
202 lwsl_notice("NULL return\n");
206 return 0; /* ongoing */
210 scan_finished(void *v)
212 struct per_vhost_data__esplws_scan *vhd = v;
213 struct per_session_data__esplws_scan *p = vhd->live_pss_list;
215 vhd->scan_ongoing = 0;
217 vhd->count_ap_records = ARRAY_SIZE(vhd->ap_records);
218 if (esp_wifi_scan_get_ap_records(&vhd->count_ap_records, vhd->ap_records) != ESP_OK) {
219 lwsl_err("%s: failed\n", __func__);
224 if (p->scan_state != SCAN_STATE_INITIAL && p->scan_state != SCAN_STATE_NONE)
225 p->changed_partway = 1;
227 p->scan_state = SCAN_STATE_INITIAL;
231 lws_callback_on_writable_all_protocol(vhd->context, vhd->protocol);
233 if (lws_esp32.inet && !vhd->cwsi && !vhd->checked_updates)
234 client_connection(vhd, "manifest.json");
236 if (vhd->changed_settings) {
237 lws_esp32_wlan_nvs_get(1);
238 vhd->changed_settings = 0;
243 static const char *ssl_names[] = { "ssl-pub.der", "ssl-pri.der" };
246 file_upload_cb(void *data, const char *name, const char *filename,
247 char *buf, int len, enum lws_spa_fileupload_states state)
249 struct per_session_data__esplws_scan *pss =
250 (struct per_session_data__esplws_scan *)data;
255 if (lws_esp32_get_reboot_type() != LWS_MAGIC_REBOOT_TYPE_FORCED_FACTORY_BUTTON)
258 lwsl_notice("LWS_UFS_OPEN Filename %s\n", filename);
259 strncpy(pss->filename, filename, sizeof(pss->filename) - 1);
260 if (!strcmp(name, "pub") || !strcmp(name, "pri")) {
261 if (nvs_open("lws-station", NVS_READWRITE, &pss->nvh))
265 pss->file_length = 0;
268 case LWS_UFS_FINAL_CONTENT:
269 case LWS_UFS_CONTENT:
271 /* if the file length is too big, drop it */
272 if (pss->file_length + len > sizeof(pss->buffer))
275 memcpy(pss->buffer + pss->file_length, buf, len);
277 pss->file_length += len;
279 if (state == LWS_UFS_CONTENT)
282 lwsl_notice("LWS_UFS_FINAL_CONTENT\n");
284 if (!strcmp(name, "pri"))
286 n = nvs_set_blob(pss->nvh, ssl_names[n], pss->buffer, pss->file_length);
288 nvs_commit(pss->nvh);
299 callback_esplws_scan(struct lws *wsi, enum lws_callback_reasons reason,
300 void *user, void *in, size_t len)
302 struct per_session_data__esplws_scan *pss =
303 (struct per_session_data__esplws_scan *)user;
304 struct per_vhost_data__esplws_scan *vhd =
305 (struct per_vhost_data__esplws_scan *)
306 lws_protocol_vh_priv_get(lws_get_vhost(wsi),
307 lws_get_protocol(wsi));
308 unsigned char *start = pss->buffer + LWS_PRE - 1, *p = start,
309 *end = pss->buffer + sizeof(pss->buffer) - 1;
318 case LWS_CALLBACK_PROTOCOL_INIT:
319 vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi),
320 lws_get_protocol(wsi),
321 sizeof(struct per_vhost_data__esplws_scan));
322 vhd->context = lws_get_context(wsi);
323 vhd->protocol = lws_get_protocol(wsi);
324 vhd->vhost = lws_get_vhost(wsi);
325 vhd->timer = xTimerCreate("x", pdMS_TO_TICKS(10000), 1, vhd,
326 (TimerCallbackFunction_t)timer_cb);
327 xTimerStart(vhd->timer, 0);
328 vhd->scan_ongoing = 0;
329 strcpy(vhd->json, " { }");
333 case LWS_CALLBACK_PROTOCOL_DESTROY:
336 xTimerStop(vhd->timer, 0);
337 xTimerDelete(vhd->timer, 0);
340 case LWS_CALLBACK_ESTABLISHED:
341 vhd->count_live_pss++;
342 pss->next = vhd->live_pss_list;
343 vhd->live_pss_list = pss;
344 /* if we have scan results, update them. Otherwise wait */
345 if (vhd->count_ap_records) {
346 pss->scan_state = SCAN_STATE_INITIAL;
347 lws_callback_on_writable(wsi);
351 case LWS_CALLBACK_SERVER_WRITEABLE:
353 if (vhd->autonomous_update_sampled) {
354 p += snprintf((char *)p, end - p,
355 " {\n \"auton\":\"1\",\n \"pos\": \"%ld\",\n"
356 " \"len\":\"%ld\"\n}\n",
358 vhd->content_length);
364 switch (pss->scan_state) {
368 struct lws_esp32_image i;
369 char img_factory[512], img_ota[512];
372 case SCAN_STATE_INITIAL:
374 gettimeofday(&t, NULL);
375 if (t.tv_sec - pss->last_send.tv_sec < 10)
380 ESP_ERROR_CHECK(nvs_open("lws-station", NVS_READWRITE, &nvh));
382 if (nvs_get_blob(nvh, "ssl-pub.der", NULL, &s) == ESP_OK)
384 if (nvs_get_blob(nvh, "ssl-pri.der", NULL, &s) == ESP_OK)
386 s = sizeof(ssid) - 1;
388 nvs_get_str(nvh, "ssid", ssid, &s);
393 * this value in the JSON is just
394 * used for UI indication. Each conditional feature confirms
395 * it itself before it allows itself to be used.
398 grt = lws_esp32_get_reboot_type();
400 esp_efuse_read_mac(mac);
401 strcpy(img_factory, " { \"date\": \"Empty\" }");
402 strcpy(img_ota, " { \"date\": \"Empty\" }");
404 lws_esp32_get_image_info(esp_partition_find_first(ESP_PARTITION_TYPE_APP,
405 ESP_PARTITION_SUBTYPE_APP_FACTORY, NULL), &i,
406 img_factory, sizeof(img_factory));
407 lws_esp32_get_image_info(esp_partition_find_first(ESP_PARTITION_TYPE_APP,
408 ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL), &i,
409 img_ota, sizeof(img_ota));
411 p += snprintf((char *)p, end - p,
412 "{ \"model\":\"%s\",\n"
413 " \"forced_button\":\"%d\",\n"
414 " \"serial\":\"%s\",\n"
415 " \"opts\":\"%s\",\n"
416 " \"host\":\"%s-%s\",\n"
417 " \"region\":\"%d\",\n"
418 " \"ssl_pub\":\"%d\",\n"
419 " \"ssl_pri\":\"%d\",\n"
420 " \"mac\":\"%02X%02X%02X%02X%02X%02X\",\n"
421 " \"ssid\":\"%s\",\n"
422 " \"conn_ip\":\"%s\",\n"
423 " \"conn_mask\":\"%s\",\n"
424 " \"conn_gw\":\"%s\",\n"
425 " \"img_factory\": %s,\n"
426 " \"img_ota\": %s,\n",
428 grt == LWS_MAGIC_REBOOT_TYPE_FORCED_FACTORY_BUTTON,
431 lws_esp32.model, lws_esp32.serial,
434 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5] | 1,
443 n = LWS_WRITE_TEXT | LWS_WRITE_NO_FIN;
444 pss->scan_state = SCAN_STATE_INITIAL_MANIFEST;
449 case SCAN_STATE_INITIAL_MANIFEST:
450 p += snprintf((char *)p, end - p,
452 " \"inet\":\"%d\",\n",
457 p += snprintf((char *)p, end - p,
460 n = LWS_WRITE_CONTINUATION | LWS_WRITE_NO_FIN;
461 pss->scan_state = SCAN_STATE_LIST;
464 case SCAN_STATE_LIST:
465 for (m = 0; m < 6; m++) {
466 n = LWS_WRITE_CONTINUATION | LWS_WRITE_NO_FIN;
467 if (pss->ap_record >= vhd->count_ap_records)
468 goto scan_state_final;
474 r = &vhd->ap_records[(int)pss->ap_record++];
475 p += snprintf((char *)p, end - p,
476 "{\"ssid\":\"%s\",\n"
477 "\"bssid\":\"%02X:%02X:%02X:%02X:%02X:%02X\",\n"
480 "\"auth\":\"%d\"}\n",
482 r->bssid[0], r->bssid[1], r->bssid[2],
483 r->bssid[3], r->bssid[4], r->bssid[5],
484 r->rssi, r->primary, r->authmode);
485 if (pss->ap_record >= vhd->count_ap_records)
486 pss->scan_state = SCAN_STATE_FINAL;
490 case SCAN_STATE_FINAL:
492 n = LWS_WRITE_CONTINUATION;
493 p += sprintf((char *)p, "]\n}\n");
494 if (pss->changed_partway) {
496 pss->scan_state = SCAN_STATE_INITIAL;
498 pss->scan_state = SCAN_STATE_NONE;
499 vhd->autonomous_update_sampled = vhd->autonomous_update;
506 // lwsl_notice("issue: %d (%d)\n", p - start, n);
507 m = lws_write(wsi, (unsigned char *)start, p - start, n);
509 lwsl_err("ERROR %d writing to di socket\n", m);
513 if (pss->scan_state != SCAN_STATE_NONE)
514 lws_callback_on_writable(wsi);
518 case LWS_CALLBACK_RECEIVE:
520 const char *sect = "\"app\": {", *b;
525 if (strstr((const char *)in, "identify")) {
526 lws_esp32_identify_physical_device();
530 if (vhd->json_len && strstr((const char *)in, "update-factory")) {
531 sect = "\"factory\": {";
534 if (vhd->json_len && strstr((const char *)in, "update-ota"))
537 if (strstr((const char *)in, "reset"))
540 if (nvs_open("lws-station", NVS_READWRITE, &nvh) != ESP_OK) {
541 lwsl_err("Unable to open nvs\n");
545 for (n = 0; n < ARRAY_SIZE(store_json); n++) {
546 if (esplws_simple_arg(p, sizeof(p), in, store_json[n].j))
549 /* only change access password if he has physical access to device */
550 if (n == 2 && lws_esp32_get_reboot_type() != LWS_MAGIC_REBOOT_TYPE_FORCED_FACTORY_BUTTON)
553 lwsl_notice("%s '%s\n", store_json[n].nvs, p);
555 if (nvs_set_str(nvh, store_json[n].nvs, p) != ESP_OK) {
556 lwsl_err("Unable to store %s in nvm\n", store_json[n].nvs);
564 if (lws_esp32_get_reboot_type() == LWS_MAGIC_REBOOT_TYPE_FORCED_FACTORY_BUTTON) {
566 if (strstr((const char *)in, "factory-reset")) {
567 ESP_ERROR_CHECK(nvs_open("lws-station", NVS_READWRITE, &nvh));
577 if (vhd->scan_ongoing)
578 vhd->changed_settings = 1;
580 lws_esp32_wlan_nvs_get(1);
582 lwsl_notice("set Join AP info\n");
591 vhd->reboot_timer = xTimerCreate("x", pdMS_TO_TICKS(250), 0, vhd,
592 (TimerCallbackFunction_t)reboot_timer_cb);
593 xTimerStart(vhd->reboot_timer, 0);
598 lwsl_notice("Autonomous upload\n");
599 b = strstr(vhd->json, sect);
601 lwsl_notice("Can't find %s in JSON\n", sect);
604 b = strstr(b, "\"file\": \"");
606 lwsl_notice("Can't find \"file\": JSON\n");
609 vhd->autonomous_update = 1;
610 if (pss->scan_state == SCAN_STATE_NONE)
611 vhd->autonomous_update_sampled = 1;
614 while ((*b != '\"') && n < sizeof(p) - 1)
619 vhd->part = ota_choose_part();
623 if (client_connection(vhd, p))
624 vhd->autonomous_update = 0;
629 case LWS_CALLBACK_CLOSED:
631 struct per_session_data__esplws_scan **p = &vhd->live_pss_list;
642 vhd->count_live_pss--;
646 /* "factory" POST handling */
648 case LWS_CALLBACK_HTTP_BODY:
649 /* create the POST argument parser if not already existing */
650 lwsl_notice("LWS_CALLBACK_HTTP_BODY (scan)\n");
652 pss->spa = lws_spa_create(wsi, param_names,
653 ARRAY_SIZE(param_names), 1024,
654 file_upload_cb, pss);
658 pss->filename[0] = '\0';
659 pss->file_length = 0;
662 /* let it parse the POST data */
663 if (lws_spa_process(pss->spa, in, len))
667 case LWS_CALLBACK_HTTP_BODY_COMPLETION:
668 lwsl_notice("LWS_CALLBACK_HTTP_BODY_COMPLETION (scan)\n");
669 /* call to inform no more payload data coming */
670 lws_spa_finalize(pss->spa);
672 if (nvs_open("lws-station", NVS_READWRITE, &nvh) != ESP_OK) {
673 lwsl_err("Unable to open nvs\n");
677 if (lws_esp32_get_reboot_type() == LWS_MAGIC_REBOOT_TYPE_FORCED_FACTORY_BUTTON) {
679 if (lws_spa_get_string(pss->spa, EPN_SERIAL)) {
680 if (nvs_set_str(nvh, "serial", lws_spa_get_string(pss->spa, EPN_SERIAL)) != ESP_OK) {
681 lwsl_err("Unable to store serial in nvm\n");
688 if (lws_spa_get_string(pss->spa, EPN_OPTS)) {
689 if (nvs_set_str(nvh, "opts", lws_spa_get_string(pss->spa, EPN_OPTS)) != ESP_OK) {
690 lwsl_err("Unable to store options in nvm\n");
699 pss->result_len = snprintf(pss->result + LWS_PRE, sizeof(pss->result) - LWS_PRE - 1,
700 "<html>Rebooting after storing certs...<br>connect to AP '<b>config-%s-%s</b>' and continue here: "
701 "<a href=\"https://192.168.4.1\">https://192.168.4.1</a></html>",
702 lws_esp32.model, lws_spa_get_string(pss->spa, EPN_SERIAL));
704 if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end))
707 if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE,
708 (unsigned char *)"text/html", 9, &p, end))
710 if (lws_add_http_header_content_length(wsi, pss->result_len, &p, end))
712 if (lws_finalize_http_header(wsi, &p, end))
715 n = lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS);
719 lws_callback_on_writable(wsi);
722 case LWS_CALLBACK_HTTP_WRITEABLE:
723 lwsl_debug("LWS_CALLBACK_HTTP_WRITEABLE: sending %d\n",
725 n = lws_write(wsi, (unsigned char *)pss->result + LWS_PRE,
726 pss->result_len, LWS_WRITE_HTTP);
730 vhd->reboot_timer = xTimerCreate("x", pdMS_TO_TICKS(3000), 0, vhd,
731 (TimerCallbackFunction_t)reboot_timer_cb);
732 xTimerStart(vhd->reboot_timer, 0);
734 return 1; // hang up since we will reset
736 /* ----- client handling ----- */
738 case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
739 lwsl_notice("Client connection error %s\n", (char *)in);
743 case LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP:
744 if (!vhd->autonomous_update)
750 if (lws_hdr_copy(wsi, pp, sizeof(pp) - 1, WSI_TOKEN_HTTP_CONTENT_LENGTH) < 0)
753 vhd->content_length = atoi(pp);
754 if (vhd->content_length <= 0 ||
755 vhd->content_length > vhd->part->size)
758 if (esp_ota_begin(vhd->part, (long)-1, &vhd->otahandle) != ESP_OK) {
759 lwsl_err("OTA: Failed to begin\n");
763 vhd->file_length = 0;
768 case LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ:
769 //lwsl_notice("LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ: %ld\n",
772 if (!vhd->autonomous_update) {
773 if (sizeof(vhd->json) - vhd->json_len - 1 < len)
774 len = sizeof(vhd->json) - vhd->json_len - 1;
775 memcpy(vhd->json + vhd->json_len, in, len);
776 vhd->json_len += len;
777 vhd->json[vhd->json_len] = '\0';
781 /* autonomous download */
784 if (vhd->file_length + len > vhd->part->size) {
785 lwsl_err("OTA: incoming file too large\n");
789 lwsl_debug("writing 0x%lx... 0x%lx\n",
790 vhd->part->address + vhd->file_length,
791 vhd->part->address + vhd->file_length + len);
792 if (esp_ota_write(vhd->otahandle, in, len) != ESP_OK) {
793 lwsl_err("OTA: Failed to write\n");
796 vhd->file_length += len;
798 lws_callback_on_writable_all_protocol(vhd->context, vhd->protocol);
802 esp_ota_end(vhd->otahandle);
804 vhd->autonomous_update = 0;
808 case LWS_CALLBACK_RECEIVE_CLIENT_HTTP:
810 char *px = (char *)pss->buffer + LWS_PRE;
811 int lenx = sizeof(pss->buffer) - LWS_PRE - 1;
813 //lwsl_notice("LWS_CALLBACK_RECEIVE_CLIENT_HTTP: %d\n", len);
815 if (lws_http_client_read(wsi, &px, &lenx) < 0)
820 case LWS_CALLBACK_COMPLETED_CLIENT_HTTP:
821 lwsl_notice("LWS_CALLBACK_COMPLETED_CLIENT_HTTP\n");
823 if (!vhd->autonomous_update) {
825 vhd->checked_updates = 1;
830 /* autonomous download */
832 lwsl_notice("auton complete\n");
834 if (esp_ota_end(vhd->otahandle) != ESP_OK) {
835 lwsl_err("OTA: end failed\n");
839 if (esp_ota_set_boot_partition(vhd->part) != ESP_OK) {
840 lwsl_err("OTA: set boot part failed\n");
844 vhd->autonomous_update = 0;
846 vhd->reboot_timer = xTimerCreate("x", pdMS_TO_TICKS(250), 0, vhd,
847 (TimerCallbackFunction_t)reboot_timer_cb);
848 xTimerStart(vhd->reboot_timer, 0);
851 case LWS_CALLBACK_CLOSED_CLIENT_HTTP:
852 lwsl_notice("LWS_CALLBACK_CLOSED_CLIENT_HTTP\n");
855 case LWS_CALLBACK_HTTP_DROP_PROTOCOL:
856 /* called when our wsi user_space is going to be destroyed */
858 lws_spa_destroy(pss->spa);
873 #define LWS_PLUGIN_PROTOCOL_ESPLWS_SCAN \
876 callback_esplws_scan, \
877 sizeof(struct per_session_data__esplws_scan), \