Hide button color elements if not supported (#2668)
[ExpressLRS.git] / src / lib / WIFI / devWIFI.cpp
blob5527bf4453acad059e01df3e045dce7c97b8ea16
1 #include "device.h"
3 #if defined(PLATFORM_ESP8266) || defined(PLATFORM_ESP32)
5 #include <AsyncJson.h>
6 #include <ArduinoJson.h>
7 #if defined(PLATFORM_ESP8266)
8 #include <FS.h>
9 #else
10 #include <SPIFFS.h>
11 #endif
13 #if defined(PLATFORM_ESP32)
14 #include <WiFi.h>
15 #include <ESPmDNS.h>
16 #include <Update.h>
17 #include <esp_partition.h>
18 #include <esp_ota_ops.h>
19 #include <soc/uart_pins.h>
20 #else
21 #include <ESP8266WiFi.h>
22 #include <ESP8266mDNS.h>
23 #define wifi_mode_t WiFiMode_t
24 #define U0TXD_GPIO_NUM (1)
25 #define U0RXD_GPIO_NUM (3)
26 #endif
27 #include <DNSServer.h>
29 #include <set>
30 #include <StreamString.h>
32 #include <ESPAsyncWebServer.h>
33 #include "AsyncJson.h"
34 #include "ArduinoJson.h"
36 #include "common.h"
37 #include "POWERMGNT.h"
38 #include "FHSS.h"
39 #include "hwTimer.h"
40 #include "logging.h"
41 #include "options.h"
42 #include "helpers.h"
43 #include "devVTXSPI.h"
44 #include "devButton.h"
46 #include "WebContent.h"
48 #include "config.h"
50 #if defined(TARGET_TX)
52 #include "wifiJoystick.h"
54 extern TxConfig config;
55 extern void setButtonColors(uint8_t b1, uint8_t b2);
56 #else
57 extern RxConfig config;
58 #endif
60 extern unsigned long rebootTime;
62 static char station_ssid[33];
63 static char station_password[65];
65 static bool wifiStarted = false;
66 bool webserverPreventAutoStart = false;
68 static wl_status_t laststatus = WL_IDLE_STATUS;
69 volatile WiFiMode_t wifiMode = WIFI_OFF;
70 static volatile WiFiMode_t changeMode = WIFI_OFF;
71 static volatile unsigned long changeTime = 0;
73 static const byte DNS_PORT = 53;
74 static IPAddress netMsk(255, 255, 255, 0);
75 static DNSServer dnsServer;
76 static IPAddress ipAddress;
77 static IPAddress gatewayIpAddress(0, 0, 0, 0);
79 #if defined(USE_MSP_WIFI) && defined(TARGET_RX) //MSP2WIFI in enabled only for RX only at the moment
80 #include "crsf2msp.h"
81 #include "msp2crsf.h"
83 #include "tcpsocket.h"
84 TCPSOCKET wifi2tcp(5761); //port 5761 as used by BF configurator
85 #endif
87 #if defined(PLATFORM_ESP8266)
88 static bool scanComplete = false;
89 #endif
91 static AsyncWebServer server(80);
92 static bool servicesStarted = false;
93 static constexpr uint32_t STALE_WIFI_SCAN = 20000;
94 static uint32_t lastScanTimeMS = 0;
96 static bool target_seen = false;
97 static uint8_t target_pos = 0;
98 static String target_found;
99 static bool target_complete = false;
100 static bool force_update = false;
101 static uint32_t totalSize;
103 void setWifiUpdateMode()
105 // No need to ExitBindingMode(), the radio will be stopped stopped when start the Wifi service.
106 // Need to change this before the mode change event so the LED is updated
107 InBindingMode = false;
108 connectionState = wifiUpdate;
111 /** Is this an IP? */
112 static boolean isIp(String str)
114 for (size_t i = 0; i < str.length(); i++)
116 int c = str.charAt(i);
117 if (c != '.' && (c < '0' || c > '9'))
119 return false;
122 return true;
125 /** IP to String? */
126 static String toStringIp(IPAddress ip)
128 String res = "";
129 for (int i = 0; i < 3; i++)
131 res += String((ip >> (8 * i)) & 0xFF) + ".";
133 res += String(((ip >> 8 * 3)) & 0xFF);
134 return res;
137 static bool captivePortal(AsyncWebServerRequest *request)
139 extern const char *wifi_hostname;
141 if (!isIp(request->host()) && request->host() != (String(wifi_hostname) + ".local"))
143 DBGLN("Request redirected to captive portal");
144 request->redirect(String("http://") + toStringIp(request->client()->localIP()));
145 return true;
147 return false;
150 static struct {
151 const char *url;
152 const char *contentType;
153 const uint8_t* content;
154 const size_t size;
155 } files[] = {
156 {"/scan.js", "text/javascript", (uint8_t *)SCAN_JS, sizeof(SCAN_JS)},
157 {"/mui.js", "text/javascript", (uint8_t *)MUI_JS, sizeof(MUI_JS)},
158 {"/elrs.css", "text/css", (uint8_t *)ELRS_CSS, sizeof(ELRS_CSS)},
159 {"/hardware.html", "text/html", (uint8_t *)HARDWARE_HTML, sizeof(HARDWARE_HTML)},
160 {"/hardware.js", "text/javascript", (uint8_t *)HARDWARE_JS, sizeof(HARDWARE_JS)},
161 {"/cw.html", "text/html", (uint8_t *)CW_HTML, sizeof(CW_HTML)},
162 {"/cw.js", "text/javascript", (uint8_t *)CW_JS, sizeof(CW_JS)},
165 static void WebUpdateSendContent(AsyncWebServerRequest *request)
167 for (size_t i=0 ; i<ARRAY_SIZE(files) ; i++) {
168 if (request->url().equals(files[i].url)) {
169 AsyncWebServerResponse *response = request->beginResponse_P(200, files[i].contentType, files[i].content, files[i].size);
170 response->addHeader("Content-Encoding", "gzip");
171 request->send(response);
172 return;
175 request->send(404, "text/plain", "File not found");
178 static void WebUpdateHandleRoot(AsyncWebServerRequest *request)
180 if (captivePortal(request))
181 { // If captive portal redirect instead of displaying the page.
182 return;
184 force_update = request->hasArg("force");
185 AsyncWebServerResponse *response;
186 if (connectionState == hardwareUndefined)
188 response = request->beginResponse_P(200, "text/html", (uint8_t*)HARDWARE_HTML, sizeof(HARDWARE_HTML));
190 else
192 response = request->beginResponse_P(200, "text/html", (uint8_t*)INDEX_HTML, sizeof(INDEX_HTML));
194 response->addHeader("Content-Encoding", "gzip");
195 response->addHeader("Cache-Control", "no-cache, no-store, must-revalidate");
196 response->addHeader("Pragma", "no-cache");
197 response->addHeader("Expires", "-1");
198 request->send(response);
201 static void putFile(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
203 static File file;
204 static size_t bytes;
205 if (!file || request->url() != file.name()) {
206 file = SPIFFS.open(request->url(), "w");
207 bytes = 0;
209 file.write(data, len);
210 bytes += len;
211 if (bytes == total) {
212 file.close();
216 static void getFile(AsyncWebServerRequest *request)
218 if (request->url() == "/options.json") {
219 request->send(200, "application/json", getOptions());
220 } else if (request->url() == "/hardware.json") {
221 request->send(200, "application/json", getHardware());
222 } else {
223 request->send(SPIFFS, request->url().c_str(), "text/plain", true);
227 static void HandleReboot(AsyncWebServerRequest *request)
229 AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "Kill -9, no more CPU time!");
230 response->addHeader("Connection", "close");
231 request->send(response);
232 request->client()->close();
233 rebootTime = millis() + 100;
236 static void HandleReset(AsyncWebServerRequest *request)
238 if (request->hasArg("hardware")) {
239 SPIFFS.remove("/hardware.json");
241 if (request->hasArg("options")) {
242 SPIFFS.remove("/options.json");
244 if (request->hasArg("model") || request->hasArg("config")) {
245 config.SetDefaults(true);
247 AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "Reset complete, rebooting...");
248 response->addHeader("Connection", "close");
249 request->send(response);
250 request->client()->close();
251 rebootTime = millis() + 100;
254 static void UpdateSettings(AsyncWebServerRequest *request, JsonVariant &json)
256 if (firmwareOptions.flash_discriminator != json["flash-discriminator"].as<uint32_t>()) {
257 request->send(409, "text/plain", "Mismatched device identifier, refresh the page and try again.");
258 return;
261 File file = SPIFFS.open("/options.json", "w");
262 serializeJson(json, file);
263 request->send(200);
266 static const char *GetConfigUidType(JsonDocument &json)
268 #if defined(TARGET_RX)
269 if (config.GetVolatileBind())
270 return "Volatile";
271 if (config.GetIsBound())
272 return "Bound";
273 return "Not Bound";
274 #else
275 if (firmwareOptions.hasUID)
277 if (json["options"]["customised"] | false)
278 return "Overridden";
279 else
280 return "Flashed";
282 return "Not set (using MAC address)";
283 #endif
286 static void GetConfiguration(AsyncWebServerRequest *request)
288 #if defined(PLATFORM_ESP32)
289 DynamicJsonDocument json(32768);
290 #else
291 DynamicJsonDocument json(2048);
292 #endif
294 bool exportMode = request->hasArg("export");
296 if (!exportMode)
298 DynamicJsonDocument options(2048);
299 deserializeJson(options, getOptions());
300 json["options"] = options;
303 JsonArray uid = json["config"].createNestedArray("uid");
304 copyArray(UID, UID_LEN, uid);
306 #if defined(TARGET_TX)
307 int button_count = 0;
308 if (GPIO_PIN_BUTTON != UNDEF_PIN)
309 button_count = 1;
310 if (GPIO_PIN_BUTTON2 != UNDEF_PIN)
311 button_count = 2;
312 for (int button=0 ; button<button_count ; button++)
314 const tx_button_color_t *buttonColor = config.GetButtonActions(button);
315 if (hardware_int(button == 0 ? HARDWARE_button_led_index : HARDWARE_button2_led_index) != -1) {
316 json["config"]["button-actions"][button]["color"] = buttonColor->val.color;
318 for (int pos=0 ; pos<button_GetActionCnt() ; pos++)
320 json["config"]["button-actions"][button]["action"][pos]["is-long-press"] = buttonColor->val.actions[pos].pressType ? true : false;
321 json["config"]["button-actions"][button]["action"][pos]["count"] = buttonColor->val.actions[pos].count;
322 json["config"]["button-actions"][button]["action"][pos]["action"] = buttonColor->val.actions[pos].action;
325 if (exportMode)
327 json["config"]["fan-mode"] = config.GetFanMode();
328 json["config"]["power-fan-threshold"] = config.GetPowerFanThreshold();
330 json["config"]["motion-mode"] = config.GetMotionMode();
332 json["config"]["vtx-admin"]["band"] = config.GetVtxBand();
333 json["config"]["vtx-admin"]["channel"] = config.GetVtxChannel();
334 json["config"]["vtx-admin"]["pitmode"] = config.GetVtxPitmode();
335 json["config"]["vtx-admin"]["power"] = config.GetVtxPower();
336 json["config"]["backpack"]["dvr-start-delay"] = config.GetDvrStartDelay();
337 json["config"]["backpack"]["dvr-stop-delay"] = config.GetDvrStopDelay();
338 json["config"]["backpack"]["dvr-aux-channel"] = config.GetDvrAux();
340 for (int model = 0 ; model < CONFIG_TX_MODEL_CNT ; model++)
342 const model_config_t &modelConfig = config.GetModelConfig(model);
343 String strModel(model);
344 const JsonObject &modelJson = json["config"]["model"].createNestedObject(strModel);
345 modelJson["packet-rate"] = modelConfig.rate;
346 modelJson["telemetry-ratio"] = modelConfig.tlm;
347 modelJson["switch-mode"] = modelConfig.switchMode;
348 modelJson["power"]["max-power"] = modelConfig.power;
349 modelJson["power"]["dynamic-power"] = modelConfig.dynamicPower;
350 modelJson["power"]["boost-channel"] = modelConfig.boostChannel;
351 modelJson["model-match"] = modelConfig.modelMatch;
352 modelJson["tx-antenna"] = modelConfig.txAntenna;
355 #endif /* TARGET_TX */
357 if (!exportMode)
359 json["config"]["ssid"] = station_ssid;
360 json["config"]["mode"] = wifiMode == WIFI_STA ? "STA" : "AP";
361 #if defined(TARGET_RX)
362 json["config"]["serial-protocol"] = config.GetSerialProtocol();
363 json["config"]["sbus-failsafe"] = config.GetFailsafeMode();
364 json["config"]["modelid"] = config.GetModelId();
365 json["config"]["force-tlm"] = config.GetForceTlmOff();
366 json["config"]["vbind"] = config.GetVolatileBind();
367 #if defined(GPIO_PIN_PWM_OUTPUTS)
368 for (int ch=0; ch<GPIO_PIN_PWM_OUTPUTS_COUNT; ++ch)
370 json["config"]["pwm"][ch]["config"] = config.GetPwmChannel(ch)->raw;
371 json["config"]["pwm"][ch]["pin"] = GPIO_PIN_PWM_OUTPUTS[ch];
372 uint8_t features = 0;
373 auto pin = GPIO_PIN_PWM_OUTPUTS[ch];
374 if (pin == U0TXD_GPIO_NUM) features |= 1; // SerialTX supported
375 else if (pin == U0RXD_GPIO_NUM) features |= 2; // SerialRX supported
376 else if (pin == GPIO_PIN_SCL) features |= 4; // I2C SCL supported (only on this pin)
377 else if (pin == GPIO_PIN_SDA) features |= 8; // I2C SCL supported (only on this pin)
378 else if (GPIO_PIN_SCL == UNDEF_PIN || GPIO_PIN_SDA == UNDEF_PIN) features |= 12; // Both I2C SCL/SDA supported (on any pin)
379 #if defined(PLATFORM_ESP32)
380 if (pin != 0) features |= 16; // DShot supported
381 #endif
382 json["config"]["pwm"][ch]["features"] = features;
384 #endif
385 #endif
386 json["config"]["product_name"] = product_name;
387 json["config"]["lua_name"] = device_name;
388 json["config"]["reg_domain"] = FHSSgetRegulatoryDomain();
389 json["config"]["has-highpower"] = (MaxPower != HighPower);
390 json["config"]["uidtype"] = GetConfigUidType(json);
393 AsyncResponseStream *response = request->beginResponseStream("application/json");
394 serializeJson(json, *response);
395 request->send(response);
398 #if defined(TARGET_TX)
399 static void UpdateConfiguration(AsyncWebServerRequest *request, JsonVariant &json)
401 if (json.containsKey("button-actions")) {
402 const JsonArray &array = json["button-actions"].as<JsonArray>();
403 for (size_t button=0 ; button<array.size() ; button++)
405 tx_button_color_t action;
406 for (int pos=0 ; pos<button_GetActionCnt() ; pos++)
408 action.val.actions[pos].pressType = array[button]["action"][pos]["is-long-press"];
409 action.val.actions[pos].count = array[button]["action"][pos]["count"];
410 action.val.actions[pos].action = array[button]["action"][pos]["action"];
412 action.val.color = array[button]["color"];
413 config.SetButtonActions(button, &action);
416 config.Commit();
417 request->send(200, "text/plain", "Import/update complete");
420 static void ImportConfiguration(AsyncWebServerRequest *request, JsonVariant &json)
422 if (json.containsKey("config"))
424 json = json["config"];
427 if (json.containsKey("fan-mode")) config.SetFanMode(json["fan-mode"]);
428 if (json.containsKey("power-fan-threshold")) config.SetPowerFanThreshold(json["power-fan-threshold"]);
429 if (json.containsKey("motion-mode")) config.SetMotionMode(json["motion-mode"]);
431 if (json.containsKey("vtx-admin"))
433 if (json["vtx-admin"].containsKey("band")) config.SetVtxBand(json["vtx-admin"]["band"]);
434 if (json["vtx-admin"].containsKey("channel")) config.SetVtxChannel(json["vtx-admin"]["channel"]);
435 if (json["vtx-admin"].containsKey("pitmode")) config.SetVtxPitmode(json["vtx-admin"]["pitmode"]);
436 if (json["vtx-admin"].containsKey("power")) config.SetVtxPower(json["vtx-admin"]["power"]);
439 if (json.containsKey("backpack"))
441 if (json["backpack"].containsKey("dvr-start-delay")) config.SetDvrStartDelay(json["backpack"]["dvr-start-delay"]);
442 if (json["backpack"].containsKey("dvr-stop-delay")) config.SetDvrStopDelay(json["backpack"]["dvr-stop-delay"]);
443 if (json["backpack"].containsKey("dvr-aux-channel")) config.SetDvrAux(json["backpack"]["dvr-aux-channel"]);
446 if (json.containsKey("model"))
448 for(const auto& kv : json["model"].as<JsonObject>())
450 uint8_t model = String(kv.key().c_str()).toInt();
451 const JsonObject &modelJson = kv.value();
453 config.SetModelId(model);
454 if (modelJson.containsKey("packet-rate")) config.SetRate(modelJson["packet-rate"]);
455 if (modelJson.containsKey("telemetry-ratio")) config.SetTlm(modelJson["telemetry-ratio"]);
456 if (modelJson.containsKey("switch-mode")) config.SetSwitchMode(modelJson["switch-mode"]);
457 if (modelJson.containsKey("power"))
459 if (modelJson["power"].containsKey("max-power")) config.SetPower(modelJson["power"]["max-power"]);
460 if (modelJson["power"].containsKey("dynamic-power")) config.SetDynamicPower(modelJson["power"]["dynamic-power"]);
461 if (modelJson["power"].containsKey("boost-channel")) config.SetBoostChannel(modelJson["power"]["boost-channel"]);
463 if (modelJson.containsKey("model-match")) config.SetModelMatch(modelJson["model-match"]);
464 // if (modelJson.containsKey("tx-antenna")) config.SetTxAntenna(modelJson["tx-antenna"]);
465 // have to commmit after each model is updated
466 config.Commit();
470 UpdateConfiguration(request, json);
473 static void WebUpdateButtonColors(AsyncWebServerRequest *request, JsonVariant &json)
475 int button1Color = json[0].as<int>();
476 int button2Color = json[1].as<int>();
477 DBGLN("%d %d", button1Color, button2Color);
478 setButtonColors(button1Color, button2Color);
479 request->send(200);
481 #else
483 * @brief: Copy uid to config if changed
485 static void JsonUidToConfig(JsonVariant &json)
487 JsonArray juid = json["uid"].as<JsonArray>();
488 size_t juidLen = constrain(juid.size(), 0, UID_LEN);
489 uint8_t newUid[UID_LEN] = { 0 };
491 // Copy only as many bytes as were included, right-justified
492 // This supports 6-digit UID as well as 4-digit (OTA bound) UID
493 copyArray(juid, &newUid[UID_LEN-juidLen], juidLen);
495 if (memcmp(newUid, config.GetUID(), UID_LEN) != 0)
497 config.SetUID(newUid);
498 config.Commit();
499 // Also copy it to the global UID in case the page is reloaded
500 memcpy(UID, newUid, UID_LEN);
503 static void UpdateConfiguration(AsyncWebServerRequest *request, JsonVariant &json)
505 uint8_t protocol = json["serial-protocol"] | 0;
506 config.SetSerialProtocol((eSerialProtocol)protocol);
508 uint8_t failsafe = json["sbus-failsafe"] | 0;
509 config.SetFailsafeMode((eFailsafeMode)failsafe);
511 long modelid = json["modelid"] | 255;
512 if (modelid < 0 || modelid > 63) modelid = 255;
513 config.SetModelId((uint8_t)modelid);
515 long forceTlm = json["force-tlm"] | 0;
516 config.SetForceTlmOff(forceTlm != 0);
518 config.SetVolatileBind((json["vbind"] | 0) != 0);
519 JsonUidToConfig(json);
521 #if defined(GPIO_PIN_PWM_OUTPUTS)
522 JsonArray pwm = json["pwm"].as<JsonArray>();
523 for(uint32_t channel = 0 ; channel < pwm.size() ; channel++)
525 uint32_t val = pwm[channel];
526 //DBGLN("PWMch(%u)=%u", channel, val);
527 config.SetPwmChannelRaw(channel, val);
529 #endif
531 config.Commit();
532 request->send(200, "text/plain", "Configuration updated");
534 #endif
536 static void WebUpdateGetTarget(AsyncWebServerRequest *request)
538 DynamicJsonDocument json(2048);
539 json["target"] = &target_name[4];
540 json["version"] = VERSION;
541 json["product_name"] = product_name;
542 json["lua_name"] = device_name;
543 json["reg_domain"] = FHSSgetRegulatoryDomain();
544 json["git-commit"] = commit;
545 #if defined(TARGET_TX)
546 json["module-type"] = "TX";
547 #endif
548 #if defined(TARGET_RX)
549 json["module-type"] = "RX";
550 #endif
551 #if defined(RADIO_SX128X)
552 json["radio-type"] = "SX128X";
553 json["has-sub-ghz"] = false;
554 #endif
555 #if defined(RADIO_SX127X)
556 json["radio-type"] = "SX127X";
557 json["has-sub-ghz"] = true;
558 #endif
559 #if defined(RADIO_LR1121)
560 json["radio-type"] = "LR1121";
561 json["has-sub-ghz"] = true;
562 #endif
564 AsyncResponseStream *response = request->beginResponseStream("application/json");
565 serializeJson(json, *response);
566 request->send(response);
569 static void WebUpdateSendNetworks(AsyncWebServerRequest *request)
571 int numNetworks = WiFi.scanComplete();
572 if (numNetworks >= 0 && millis() - lastScanTimeMS < STALE_WIFI_SCAN) {
573 DBGLN("Found %d networks", numNetworks);
574 std::set<String> vs;
575 String s="[";
576 for(int i=0 ; i<numNetworks ; i++) {
577 String w = WiFi.SSID(i);
578 DBGLN("found %s", w.c_str());
579 if (vs.find(w)==vs.end() && w.length()>0) {
580 if (!vs.empty()) s += ",";
581 s += "\"" + w + "\"";
582 vs.insert(w);
585 s+="]";
586 request->send(200, "application/json", s);
587 } else {
588 if (WiFi.scanComplete() != WIFI_SCAN_RUNNING)
590 #if defined(PLATFORM_ESP8266)
591 scanComplete = false;
592 WiFi.scanNetworksAsync([](int){
593 scanComplete = true;
595 #else
596 WiFi.scanNetworks(true);
597 #endif
598 lastScanTimeMS = millis();
600 request->send(204, "application/json", "[]");
604 static void sendResponse(AsyncWebServerRequest *request, const String &msg, WiFiMode_t mode) {
605 AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", msg);
606 response->addHeader("Connection", "close");
607 request->send(response);
608 request->client()->close();
609 changeTime = millis();
610 changeMode = mode;
613 static void WebUpdateAccessPoint(AsyncWebServerRequest *request)
615 DBGLN("Starting Access Point");
616 String msg = String("Access Point starting, please connect to access point '") + wifi_ap_ssid + "' with password '" + wifi_ap_password + "'";
617 sendResponse(request, msg, WIFI_AP);
620 static void WebUpdateConnect(AsyncWebServerRequest *request)
622 DBGLN("Connecting to network");
623 String msg = String("Connecting to network '") + station_ssid + "', connect to http://" +
624 wifi_hostname + ".local from a browser on that network";
625 sendResponse(request, msg, WIFI_STA);
628 static void WebUpdateSetHome(AsyncWebServerRequest *request)
630 String ssid = request->arg("network");
631 String password = request->arg("password");
633 DBGLN("Setting network %s", ssid.c_str());
634 strcpy(station_ssid, ssid.c_str());
635 strcpy(station_password, password.c_str());
636 if (request->hasArg("save")) {
637 strlcpy(firmwareOptions.home_wifi_ssid, ssid.c_str(), sizeof(firmwareOptions.home_wifi_ssid));
638 strlcpy(firmwareOptions.home_wifi_password, password.c_str(), sizeof(firmwareOptions.home_wifi_password));
639 saveOptions();
641 WebUpdateConnect(request);
644 static void WebUpdateForget(AsyncWebServerRequest *request)
646 DBGLN("Forget network");
647 firmwareOptions.home_wifi_ssid[0] = 0;
648 firmwareOptions.home_wifi_password[0] = 0;
649 saveOptions();
650 station_ssid[0] = 0;
651 station_password[0] = 0;
652 String msg = String("Home network forgotten, please connect to access point '") + wifi_ap_ssid + "' with password '" + wifi_ap_password + "'";
653 sendResponse(request, msg, WIFI_AP);
656 static void WebUpdateHandleNotFound(AsyncWebServerRequest *request)
658 if (captivePortal(request))
659 { // If captive portal redirect instead of displaying the error page.
660 return;
662 String message = F("File Not Found\n\n");
663 message += F("URI: ");
664 message += request->url();
665 message += F("\nMethod: ");
666 message += (request->method() == HTTP_GET) ? "GET" : "POST";
667 message += F("\nArguments: ");
668 message += request->args();
669 message += F("\n");
671 for (uint8_t i = 0; i < request->args(); i++)
673 message += String(F(" ")) + request->argName(i) + F(": ") + request->arg(i) + F("\n");
675 AsyncWebServerResponse *response = request->beginResponse(404, "text/plain", message);
676 response->addHeader("Cache-Control", "no-cache, no-store, must-revalidate");
677 response->addHeader("Pragma", "no-cache");
678 response->addHeader("Expires", "-1");
679 request->send(response);
682 static void corsPreflightResponse(AsyncWebServerRequest *request) {
683 AsyncWebServerResponse *response = request->beginResponse(204, "text/plain");
684 request->send(response);
687 static void WebUploadResponseHandler(AsyncWebServerRequest *request) {
688 if (target_seen || Update.hasError()) {
689 String msg;
690 if (!Update.hasError() && Update.end()) {
691 DBGLN("Update complete, rebooting");
692 msg = String("{\"status\": \"ok\", \"msg\": \"Update complete. ");
693 #if defined(TARGET_RX)
694 msg += "Please wait for the LED to resume blinking before disconnecting power.\"}";
695 #else
696 msg += "Please wait for a few seconds while the device reboots.\"}";
697 #endif
698 rebootTime = millis() + 200;
699 } else {
700 StreamString p = StreamString();
701 if (Update.hasError()) {
702 Update.printError(p);
703 } else {
704 p.println("Not enough data uploaded!");
706 p.trim();
707 DBGLN("Failed to upload firmware: %s", p.c_str());
708 msg = String("{\"status\": \"error\", \"msg\": \"") + p + "\"}";
710 AsyncWebServerResponse *response = request->beginResponse(200, "application/json", msg);
711 response->addHeader("Connection", "close");
712 request->send(response);
713 request->client()->close();
714 } else {
715 String message = String("{\"status\": \"mismatch\", \"msg\": \"<b>Current target:</b> ") + (const char *)&target_name[4] + ".<br>";
716 if (target_found.length() != 0) {
717 message += "<b>Uploaded image:</b> " + target_found + ".<br/>";
719 message += "<br/>It looks like you are flashing firmware with a different name to the current firmware. This sometimes happens because the hardware was flashed from the factory with an early version that has a different name. Or it may have even changed between major releases.";
720 message += "<br/><br/>Please double check you are uploading the correct target, then proceed with 'Flash Anyway'.\"}";
721 request->send(200, "application/json", message);
725 static void WebUploadDataHandler(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) {
726 force_update = force_update || request->hasArg("force");
727 if (index == 0) {
728 #ifdef HAS_WIFI_JOYSTICK
729 WifiJoystick::StopJoystickService();
730 #endif
732 size_t filesize = request->header("X-FileSize").toInt();
733 DBGLN("Update: '%s' size %u", filename.c_str(), filesize);
734 #if defined(PLATFORM_ESP8266)
735 Update.runAsync(true);
736 uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
737 DBGLN("Free space = %u", maxSketchSpace);
738 UNUSED(maxSketchSpace); // for warning
739 #endif
740 if (!Update.begin(filesize, U_FLASH)) { // pass the size provided
741 Update.printError(LOGGING_UART);
743 target_seen = false;
744 target_found.clear();
745 target_complete = false;
746 target_pos = 0;
747 totalSize = 0;
749 if (len) {
750 DBGVLN("writing %d", len);
751 if (Update.write(data, len) == len) {
752 if (force_update || (totalSize == 0 && *data == 0x1F))
753 target_seen = true;
754 if (!target_seen) {
755 for (size_t i=0 ; i<len ;i++) {
756 if (!target_complete && (target_pos >= 4 || target_found.length() > 0)) {
757 if (target_pos == 4) {
758 target_found.clear();
760 if (data[i] == 0 || target_found.length() > 50) {
761 target_complete = true;
763 else {
764 target_found += (char)data[i];
767 if (data[i] == target_name[target_pos]) {
768 ++target_pos;
769 if (target_pos >= target_name_size) {
770 target_seen = true;
773 else {
774 target_pos = 0; // Startover
778 totalSize += len;
779 } else {
780 DBGLN("write failed to write %d", len);
785 static void WebUploadForceUpdateHandler(AsyncWebServerRequest *request) {
786 target_seen = true;
787 if (request->arg("action").equals("confirm")) {
788 WebUploadResponseHandler(request);
789 } else {
790 #if defined(PLATFORM_ESP32)
791 Update.abort();
792 #endif
793 request->send(200, "application/json", "{\"status\": \"ok\", \"msg\": \"Update cancelled\"}");
797 #ifdef HAS_WIFI_JOYSTICK
798 static void WebUdpControl(AsyncWebServerRequest *request)
800 const String &action = request->arg("action");
801 if (action.equals("joystick_begin"))
803 WifiJoystick::StartSending(request->client()->remoteIP(),
804 request->arg("interval").toInt(), request->arg("channels").toInt());
805 request->send(200, "text/plain", "ok");
807 else if (action.equals("joystick_end"))
809 WifiJoystick::StopSending();
810 request->send(200, "text/plain", "ok");
813 #endif
815 static size_t firmwareOffset = 0;
816 static size_t getFirmwareChunk(uint8_t *data, size_t len, size_t pos)
818 uint8_t *dst;
819 uint8_t alignedBuffer[7];
820 if ((uintptr_t)data % 4 != 0)
822 // If data is not aligned, read aligned byes using the local buffer and hope the next call will be aligned
823 dst = (uint8_t *)((uint32_t)alignedBuffer / 4 * 4);
824 len = 4;
826 else
828 // Otherwise just make sure len is a multiple of 4 and smaller than a sector
829 dst = data;
830 len = constrain((len / 4) * 4, 4, SPI_FLASH_SEC_SIZE);
833 ESP.flashRead(firmwareOffset + pos, (uint32_t *)dst, len);
835 // If using local stack buffer, move the 4 bytes into the passed buffer
836 // data is known to not be aligned so it is moved byte-by-byte instead of as uint32_t*
837 if ((void *)dst != (void *)data)
839 for (unsigned b=len; b>0; --b)
840 *data++ = *dst++;
842 return len;
845 static void WebUpdateGetFirmware(AsyncWebServerRequest *request) {
846 #if defined(PLATFORM_ESP32)
847 const esp_partition_t *running = esp_ota_get_running_partition();
848 if (running) {
849 firmwareOffset = running->address;
851 #endif
852 const size_t firmwareTrailerSize = 4096; // max number of bytes for the options/hardware layout json
853 AsyncWebServerResponse *response = request->beginResponse("application/octet-stream", (size_t)ESP.getSketchSize() + firmwareTrailerSize, &getFirmwareChunk);
854 String filename = String("attachment; filename=\"") + (const char *)&target_name[4] + "_" + VERSION + ".bin\"";
855 response->addHeader("Content-Disposition", filename);
856 request->send(response);
859 #ifdef RADIO_SX128X
860 static void HandleContinuousWave(AsyncWebServerRequest *request) {
861 if (request->hasArg("radio")) {
862 SX12XX_Radio_Number_t radio = request->arg("radio").toInt() == 1 ? SX12XX_Radio_1 : SX12XX_Radio_2;
864 AsyncWebServerResponse *response = request->beginResponse(204);
865 response->addHeader("Connection", "close");
866 request->send(response);
867 request->client()->close();
869 Radio.TXdoneCallback = [](){};
870 Radio.Begin(FHSSgetMinimumFreq(), FHSSgetMaximumFreq());
872 POWERMGNT::init();
873 POWERMGNT::setPower(POWERMGNT::getMinPower());
875 Radio.startCWTest(2440000000, radio);
876 } else {
877 int radios = (GPIO_PIN_NSS_2 == UNDEF_PIN) ? 1 : 2;
878 request->send(200, "application/json", String("{\"radios\": ") + radios + "}");
881 #endif
883 static void initialize()
885 wifiStarted = false;
886 WiFi.disconnect(true);
887 WiFi.mode(WIFI_OFF);
888 #if defined(PLATFORM_ESP8266)
889 WiFi.forceSleepBegin();
890 #endif
891 registerButtonFunction(ACTION_START_WIFI, [](){
892 setWifiUpdateMode();
893 devicesTriggerEvent();
897 static void startWiFi(unsigned long now)
899 if (wifiStarted) {
900 return;
903 if (connectionState < FAILURE_STATES) {
904 hwTimer::stop();
906 #ifdef HAS_VTX_SPI
907 disableVTxSpi();
908 #endif
910 // Set transmit power to minimum
911 POWERMGNT::setPower(MinPower);
913 setWifiUpdateMode();
915 DBGLN("Stopping Radio");
916 Radio.End();
919 DBGLN("Begin Webupdater");
921 WiFi.persistent(false);
922 WiFi.disconnect();
923 WiFi.mode(WIFI_OFF);
924 strcpy(station_ssid, firmwareOptions.home_wifi_ssid);
925 strcpy(station_password, firmwareOptions.home_wifi_password);
926 if (station_ssid[0] == 0) {
927 changeTime = now;
928 changeMode = WIFI_AP;
930 else {
931 changeTime = now;
932 changeMode = WIFI_STA;
934 laststatus = WL_DISCONNECTED;
935 wifiStarted = true;
938 static void startMDNS()
940 if (!MDNS.begin(wifi_hostname))
942 DBGLN("Error starting mDNS");
943 return;
946 String options = "-DAUTO_WIFI_ON_INTERVAL=" + String(firmwareOptions.wifi_auto_on_interval / 1000);
948 #ifdef TARGET_TX
949 if (firmwareOptions.unlock_higher_power)
951 options += " -DUNLOCK_HIGHER_POWER";
953 options += " -DTLM_REPORT_INTERVAL_MS=" + String(firmwareOptions.tlm_report_interval);
954 options += " -DFAN_MIN_RUNTIME=" + String(firmwareOptions.fan_min_runtime);
955 #endif
957 #ifdef TARGET_RX
958 if (firmwareOptions.lock_on_first_connection)
960 options += " -DLOCK_ON_FIRST_CONNECTION";
962 options += " -DRCVR_UART_BAUD=" + String(firmwareOptions.uart_baud);
963 #endif
965 String instance = String(wifi_hostname) + "_" + WiFi.macAddress();
966 instance.replace(":", "");
967 #ifdef PLATFORM_ESP8266
968 // We have to do it differently on ESP8266 as setInstanceName has the side-effect of chainging the hostname!
969 MDNS.setInstanceName(wifi_hostname);
970 MDNSResponder::hMDNSService service = MDNS.addService(instance.c_str(), "http", "tcp", 80);
971 MDNS.addServiceTxt(service, "vendor", "elrs");
972 MDNS.addServiceTxt(service, "target", (const char *)&target_name[4]);
973 MDNS.addServiceTxt(service, "device", (const char *)device_name);
974 MDNS.addServiceTxt(service, "product", (const char *)product_name);
975 MDNS.addServiceTxt(service, "version", VERSION);
976 MDNS.addServiceTxt(service, "options", options.c_str());
977 MDNS.addServiceTxt(service, "type", "rx");
978 // If the probe result fails because there is another device on the network with the same name
979 // use our unique instance name as the hostname. A better way to do this would be to use
980 // MDNSResponder::indexDomain and change wifi_hostname as well.
981 MDNS.setHostProbeResultCallback([instance](const char* p_pcDomainName, bool p_bProbeResult) {
982 if (!p_bProbeResult) {
983 WiFi.hostname(instance);
984 MDNS.setInstanceName(instance);
987 #else
988 MDNS.setInstanceName(instance);
989 MDNS.addService("http", "tcp", 80);
990 MDNS.addServiceTxt("http", "tcp", "vendor", "elrs");
991 MDNS.addServiceTxt("http", "tcp", "target", (const char *)&target_name[4]);
992 MDNS.addServiceTxt("http", "tcp", "device", (const char *)device_name);
993 MDNS.addServiceTxt("http", "tcp", "product", (const char *)product_name);
994 MDNS.addServiceTxt("http", "tcp", "version", VERSION);
995 MDNS.addServiceTxt("http", "tcp", "options", options.c_str());
996 #ifdef TARGET_TX
997 MDNS.addServiceTxt("http", "tcp", "type", "tx");
998 #else
999 MDNS.addServiceTxt("http", "tcp", "type", "rx");
1000 #endif
1001 #endif
1003 #ifdef HAS_WIFI_JOYSTICK
1004 MDNS.addService("elrs", "udp", JOYSTICK_PORT);
1005 MDNS.addServiceTxt("elrs", "udp", "device", (const char *)device_name);
1006 MDNS.addServiceTxt("elrs", "udp", "version", String(JOYSTICK_VERSION).c_str());
1007 #endif
1010 static void startServices()
1012 if (servicesStarted) {
1013 #if defined(PLATFORM_ESP32)
1014 MDNS.end();
1015 startMDNS();
1016 #endif
1017 return;
1020 server.on("/", WebUpdateHandleRoot);
1021 server.on("/elrs.css", WebUpdateSendContent);
1022 server.on("/mui.js", WebUpdateSendContent);
1023 server.on("/scan.js", WebUpdateSendContent);
1024 server.on("/networks.json", WebUpdateSendNetworks);
1025 server.on("/sethome", WebUpdateSetHome);
1026 server.on("/forget", WebUpdateForget);
1027 server.on("/connect", WebUpdateConnect);
1028 server.on("/config", HTTP_GET, GetConfiguration);
1029 server.on("/access", WebUpdateAccessPoint);
1030 server.on("/target", WebUpdateGetTarget);
1031 server.on("/firmware.bin", WebUpdateGetFirmware);
1033 server.on("/generate_204", WebUpdateHandleRoot); // handle Andriod phones doing shit to detect if there is 'real' internet and possibly dropping conn.
1034 server.on("/gen_204", WebUpdateHandleRoot);
1035 server.on("/library/test/success.html", WebUpdateHandleRoot);
1036 server.on("/hotspot-detect.html", WebUpdateHandleRoot);
1037 server.on("/connectivity-check.html", WebUpdateHandleRoot);
1038 server.on("/check_network_status.txt", WebUpdateHandleRoot);
1039 server.on("/ncsi.txt", WebUpdateHandleRoot);
1040 server.on("/fwlink", WebUpdateHandleRoot);
1042 server.on("/update", HTTP_POST, WebUploadResponseHandler, WebUploadDataHandler);
1043 server.on("/update", HTTP_OPTIONS, corsPreflightResponse);
1044 server.on("/forceupdate", WebUploadForceUpdateHandler);
1045 server.on("/forceupdate", HTTP_OPTIONS, corsPreflightResponse);
1046 #ifdef RADIO_SX128X
1047 server.on("/cw.html", WebUpdateSendContent);
1048 server.on("/cw.js", WebUpdateSendContent);
1049 server.on("/cw", HandleContinuousWave);
1050 #endif
1052 DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
1053 DefaultHeaders::Instance().addHeader("Access-Control-Max-Age", "600");
1054 DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS");
1055 DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "*");
1057 server.on("/hardware.html", WebUpdateSendContent);
1058 server.on("/hardware.js", WebUpdateSendContent);
1059 server.on("/hardware.json", getFile).onBody(putFile);
1060 server.on("/options.json", HTTP_GET, getFile);
1061 server.on("/reboot", HandleReboot);
1062 server.on("/reset", HandleReset);
1063 #ifdef HAS_WIFI_JOYSTICK
1064 server.on("/udpcontrol", HTTP_POST, WebUdpControl);
1065 #endif
1067 server.addHandler(new AsyncCallbackJsonWebHandler("/config", UpdateConfiguration));
1068 server.addHandler(new AsyncCallbackJsonWebHandler("/options.json", UpdateSettings));
1069 #if defined(TARGET_TX)
1070 server.addHandler(new AsyncCallbackJsonWebHandler("/buttons", WebUpdateButtonColors));
1071 server.addHandler(new AsyncCallbackJsonWebHandler("/import", ImportConfiguration, 32768U));
1072 #endif
1074 server.onNotFound(WebUpdateHandleNotFound);
1076 server.begin();
1078 dnsServer.start(DNS_PORT, "*", ipAddress);
1079 dnsServer.setErrorReplyCode(DNSReplyCode::NoError);
1081 startMDNS();
1083 #ifdef HAS_WIFI_JOYSTICK
1084 WifiJoystick::StartJoystickService();
1085 #endif
1087 servicesStarted = true;
1088 DBGLN("HTTPUpdateServer ready! Open http://%s.local in your browser", wifi_hostname);
1089 #if defined(USE_MSP_WIFI) && defined(TARGET_RX)
1090 wifi2tcp.begin();
1091 #endif
1094 static void HandleWebUpdate()
1096 unsigned long now = millis();
1097 wl_status_t status = WiFi.status();
1099 if (status != laststatus && wifiMode == WIFI_STA) {
1100 DBGLN("WiFi status %d", status);
1101 switch(status) {
1102 case WL_NO_SSID_AVAIL:
1103 case WL_CONNECT_FAILED:
1104 case WL_CONNECTION_LOST:
1105 changeTime = now;
1106 changeMode = WIFI_AP;
1107 break;
1108 case WL_DISCONNECTED: // try reconnection
1109 changeTime = now;
1110 break;
1111 default:
1112 break;
1114 laststatus = status;
1116 if (status != WL_CONNECTED && wifiMode == WIFI_STA && (now - changeTime) > 30000) {
1117 changeTime = now;
1118 changeMode = WIFI_AP;
1119 DBGLN("Connection failed %d", status);
1121 if (changeMode != wifiMode && changeMode != WIFI_OFF && (now - changeTime) > 500) {
1122 switch(changeMode) {
1123 case WIFI_AP:
1124 DBGLN("Changing to AP mode");
1125 WiFi.disconnect();
1126 wifiMode = WIFI_AP;
1127 #if defined(PLATFORM_ESP32)
1128 WiFi.setHostname(wifi_hostname); // hostname must be set before the mode is set to STA
1129 #endif
1130 WiFi.mode(wifiMode);
1131 #if defined(PLATFORM_ESP8266)
1132 WiFi.setHostname(wifi_hostname); // hostname must be set before the mode is set to STA
1133 #endif
1134 changeTime = now;
1135 #if defined(PLATFORM_ESP8266)
1136 WiFi.setOutputPower(20.5);
1137 WiFi.setPhyMode(WIFI_PHY_MODE_11N);
1138 #elif defined(PLATFORM_ESP32)
1139 WiFi.setTxPower(WIFI_POWER_19_5dBm);
1140 #endif
1141 WiFi.softAPConfig(ipAddress, gatewayIpAddress, netMsk);
1142 WiFi.softAP(wifi_ap_ssid, wifi_ap_password);
1143 startServices();
1144 break;
1145 case WIFI_STA:
1146 DBGLN("Connecting to network '%s'", station_ssid);
1147 wifiMode = WIFI_STA;
1148 #if defined(PLATFORM_ESP32)
1149 WiFi.setHostname(wifi_hostname); // hostname must be set before the mode is set to STA
1150 #endif
1151 WiFi.mode(wifiMode);
1152 #if defined(PLATFORM_ESP8266)
1153 WiFi.setHostname(wifi_hostname); // hostname must be set after the mode is set to STA
1154 #endif
1155 changeTime = now;
1156 #if defined(PLATFORM_ESP8266)
1157 WiFi.setOutputPower(20.5);
1158 WiFi.setPhyMode(WIFI_PHY_MODE_11N);
1159 #elif defined(PLATFORM_ESP32)
1160 WiFi.setTxPower(WIFI_POWER_19_5dBm);
1161 WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL);
1162 WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN);
1163 #endif
1164 WiFi.begin(station_ssid, station_password);
1165 startServices();
1166 default:
1167 break;
1169 #if defined(PLATFORM_ESP8266)
1170 MDNS.notifyAPChange();
1171 #endif
1172 changeMode = WIFI_OFF;
1175 #if defined(PLATFORM_ESP8266)
1176 if (scanComplete)
1178 WiFi.mode(wifiMode);
1179 scanComplete = false;
1181 #endif
1183 if (servicesStarted)
1185 dnsServer.processNextRequest();
1186 #if defined(PLATFORM_ESP8266)
1187 MDNS.update();
1188 #endif
1190 #ifdef HAS_WIFI_JOYSTICK
1191 WifiJoystick::Loop(now);
1192 #endif
1196 void HandleMSP2WIFI()
1198 #if defined(USE_MSP_WIFI) && defined(TARGET_RX)
1199 // check is there is any data to write out
1200 if (crsf2msp.FIFOout.peekSize() > 0)
1202 const uint16_t len = crsf2msp.FIFOout.popSize();
1203 uint8_t data[len];
1204 crsf2msp.FIFOout.popBytes(data, len);
1205 wifi2tcp.write(data, len);
1208 // check if there is any data to read in
1209 const uint16_t bytesReady = wifi2tcp.bytesReady();
1210 if (bytesReady > 0)
1212 uint8_t data[bytesReady];
1213 wifi2tcp.read(data);
1214 msp2crsf.parse(data, bytesReady);
1217 wifi2tcp.handle();
1218 #endif
1221 static int start()
1223 ipAddress.fromString(wifi_ap_address);
1224 return firmwareOptions.wifi_auto_on_interval;
1227 static int event()
1229 if (connectionState == wifiUpdate || connectionState > FAILURE_STATES)
1231 if (!wifiStarted) {
1232 startWiFi(millis());
1233 return DURATION_IMMEDIATELY;
1236 else if (wifiStarted)
1238 wifiStarted = false;
1239 WiFi.disconnect(true);
1240 WiFi.mode(WIFI_OFF);
1241 #if defined(PLATFORM_ESP8266)
1242 WiFi.forceSleepBegin();
1243 #endif
1245 return DURATION_IGNORE;
1248 static int timeout()
1250 if (wifiStarted)
1252 HandleWebUpdate();
1253 HandleMSP2WIFI();
1254 #if defined(PLATFORM_ESP8266)
1255 // When in STA mode, a small delay reduces power use from 90mA to 30mA when idle
1256 // In AP mode, it doesn't seem to make a measurable difference, but does not hurt
1257 // Only done on 8266 as the ESP32 runs a throttled task
1258 if (!Update.isRunning())
1259 delay(1);
1260 return DURATION_IMMEDIATELY;
1261 #else
1262 // All the web traffic is async apart from changing modes and MSP2WIFI
1263 // No need to run balls-to-the-wall; the wifi runs on this core too (0)
1264 return 2;
1265 #endif
1268 #if defined(TARGET_TX)
1269 // if webupdate was requested before or .wifi_auto_on_interval has elapsed but uart is not detected
1270 // start webupdate, there might be wrong configuration flashed.
1271 if(firmwareOptions.wifi_auto_on_interval != -1 && webserverPreventAutoStart == false && connectionState < wifiUpdate && !wifiStarted){
1272 DBGLN("No CRSF ever detected, starting WiFi");
1273 setWifiUpdateMode();
1274 return DURATION_IMMEDIATELY;
1276 #elif defined(TARGET_RX)
1277 if (firmwareOptions.wifi_auto_on_interval != -1 && !webserverPreventAutoStart && (connectionState == disconnected))
1279 static bool pastAutoInterval = false;
1280 // If InBindingMode then wait at least 60 seconds before going into wifi,
1281 // regardless of if .wifi_auto_on_interval is set to less
1282 if (!InBindingMode || firmwareOptions.wifi_auto_on_interval >= 60000 || pastAutoInterval)
1284 setWifiUpdateMode();
1285 return DURATION_IMMEDIATELY;
1287 pastAutoInterval = true;
1288 return (60000 - firmwareOptions.wifi_auto_on_interval);
1290 #endif
1291 return DURATION_NEVER;
1294 device_t WIFI_device = {
1295 .initialize = initialize,
1296 .start = start,
1297 .event = event,
1298 .timeout = timeout
1301 #endif