Fork of the espurna firmware for `mhsw` switches
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

778 lines
21 KiB

6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
  1. /*
  2. WEBSOCKET MODULE
  3. Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
  4. */
  5. #if WEB_SUPPORT
  6. #include <ESPAsyncTCP.h>
  7. #include <ESPAsyncWebServer.h>
  8. #include <ArduinoJson.h>
  9. #include <Ticker.h>
  10. #include <vector>
  11. #include "libs/WebSocketIncommingBuffer.h"
  12. AsyncWebSocket _ws("/ws");
  13. Ticker _web_defer;
  14. // -----------------------------------------------------------------------------
  15. // WS callbacks
  16. // -----------------------------------------------------------------------------
  17. ws_callbacks_t& ws_callbacks_t::onVisible(ws_on_send_callback_f cb) {
  18. on_visible.push_back(cb);
  19. return *this;
  20. }
  21. ws_callbacks_t& ws_callbacks_t::onConnected(ws_on_send_callback_f cb) {
  22. on_connected.push_back(cb);
  23. return *this;
  24. }
  25. ws_callbacks_t& ws_callbacks_t::onData(ws_on_send_callback_f cb) {
  26. on_data.push_back(cb);
  27. return *this;
  28. }
  29. ws_callbacks_t& ws_callbacks_t::onAction(ws_on_action_callback_f cb) {
  30. on_action.push_back(cb);
  31. return *this;
  32. }
  33. ws_callbacks_t& ws_callbacks_t::onKeyCheck(ws_on_keycheck_callback_f cb) {
  34. on_keycheck.push_back(cb);
  35. return *this;
  36. }
  37. ws_callbacks_t _ws_callbacks;
  38. struct ws_counter_t {
  39. ws_counter_t() : current(0), start(0), stop(0) {}
  40. ws_counter_t(uint32_t start, uint32_t stop) :
  41. current(start), start(start), stop(stop) {}
  42. void reset() {
  43. current = start;
  44. }
  45. void next() {
  46. if (current < stop) {
  47. ++current;
  48. }
  49. }
  50. bool done() {
  51. return (current >= stop);
  52. }
  53. uint32_t current;
  54. uint32_t start;
  55. uint32_t stop;
  56. };
  57. struct ws_data_t {
  58. enum mode_t {
  59. SEQUENCE,
  60. ALL
  61. };
  62. ws_data_t(const ws_on_send_callback_f& cb) :
  63. storage(new ws_on_send_callback_list_t {cb}),
  64. client_id(0),
  65. mode(ALL),
  66. callbacks(*storage.get()),
  67. counter(0, 1)
  68. {}
  69. ws_data_t(const uint32_t client_id, const ws_on_send_callback_list_t& callbacks, mode_t mode = SEQUENCE) :
  70. client_id(client_id),
  71. mode(mode),
  72. callbacks(callbacks),
  73. counter(0, callbacks.size())
  74. {}
  75. bool done() {
  76. return counter.done();
  77. }
  78. void sendAll(JsonObject& root) {
  79. while (!counter.done()) counter.next();
  80. for (auto& callback : callbacks) {
  81. callback(root);
  82. }
  83. }
  84. void sendCurrent(JsonObject& root) {
  85. callbacks[counter.current](root);
  86. counter.next();
  87. }
  88. void send(JsonObject& root) {
  89. switch (mode) {
  90. case SEQUENCE: sendCurrent(root); break;
  91. case ALL: sendAll(root); break;
  92. }
  93. }
  94. std::unique_ptr<ws_on_send_callback_list_t> storage;
  95. const uint32_t client_id;
  96. const mode_t mode;
  97. const ws_on_send_callback_list_t& callbacks;
  98. ws_counter_t counter;
  99. };
  100. std::queue<ws_data_t> _ws_client_data;
  101. // -----------------------------------------------------------------------------
  102. // WS authentication
  103. // -----------------------------------------------------------------------------
  104. struct ws_ticket_t {
  105. IPAddress ip;
  106. unsigned long timestamp = 0;
  107. };
  108. ws_ticket_t _ws_tickets[WS_BUFFER_SIZE];
  109. void _onAuth(AsyncWebServerRequest *request) {
  110. webLog(request);
  111. if (!webAuthenticate(request)) return request->requestAuthentication();
  112. IPAddress ip = request->client()->remoteIP();
  113. unsigned long now = millis();
  114. unsigned short index;
  115. for (index = 0; index < WS_BUFFER_SIZE; index++) {
  116. if (_ws_tickets[index].ip == ip) break;
  117. if (_ws_tickets[index].timestamp == 0) break;
  118. if (now - _ws_tickets[index].timestamp > WS_TIMEOUT) break;
  119. }
  120. if (index == WS_BUFFER_SIZE) {
  121. request->send(429);
  122. } else {
  123. _ws_tickets[index].ip = ip;
  124. _ws_tickets[index].timestamp = now;
  125. request->send(200, "text/plain", "OK");
  126. }
  127. }
  128. bool _wsAuth(AsyncWebSocketClient * client) {
  129. IPAddress ip = client->remoteIP();
  130. unsigned long now = millis();
  131. unsigned short index = 0;
  132. for (index = 0; index < WS_BUFFER_SIZE; index++) {
  133. if ((_ws_tickets[index].ip == ip) && (now - _ws_tickets[index].timestamp < WS_TIMEOUT)) break;
  134. }
  135. if (index == WS_BUFFER_SIZE) {
  136. return false;
  137. }
  138. return true;
  139. }
  140. // -----------------------------------------------------------------------------
  141. // Debug
  142. // -----------------------------------------------------------------------------
  143. #if DEBUG_WEB_SUPPORT
  144. struct ws_debug_msg_t {
  145. ws_debug_msg_t(const char* prefix, const char* message) :
  146. prefix(prefix), message(message)
  147. {}
  148. String prefix;
  149. String message;
  150. };
  151. struct ws_debug_t {
  152. ws_debug_t(size_t capacity) :
  153. flush(false),
  154. current(0),
  155. capacity(capacity)
  156. {
  157. messages.reserve(capacity);
  158. }
  159. void add(const char* prefix, const char* message) {
  160. if (current >= capacity) {
  161. flush = true;
  162. send();
  163. }
  164. messages.emplace(messages.begin() + current, prefix, message);
  165. flush = true;
  166. ++current;
  167. }
  168. void send() {
  169. if (!flush) return;
  170. // ref: http://arduinojson.org/v5/assistant/
  171. // {"weblog": {"msg":[...],"pre":[...]}}
  172. DynamicJsonBuffer jsonBuffer(2*JSON_ARRAY_SIZE(messages.size()) + JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2));
  173. JsonObject& root = jsonBuffer.createObject();
  174. JsonObject& weblog = root.createNestedObject("weblog");
  175. JsonArray& msg = weblog.createNestedArray("msg");
  176. JsonArray& pre = weblog.createNestedArray("pre");
  177. for (auto& message : messages) {
  178. pre.add(message.prefix.c_str());
  179. msg.add(message.message.c_str());
  180. }
  181. wsSend(root);
  182. messages.clear();
  183. current = 0;
  184. flush = false;
  185. }
  186. bool flush;
  187. size_t current;
  188. const size_t capacity;
  189. std::vector<ws_debug_msg_t> messages;
  190. };
  191. // TODO: move to the headers?
  192. constexpr const size_t WS_DEBUG_MSG_BUFFER = 8;
  193. ws_debug_t _ws_debug(WS_DEBUG_MSG_BUFFER);
  194. bool wsDebugSend(const char* prefix, const char* message) {
  195. if (!wsConnected()) return false;
  196. _ws_debug.add(prefix, message);
  197. return true;
  198. }
  199. #endif
  200. // Check the existing setting before saving it
  201. // TODO: this should know of the default values, somehow?
  202. // TODO: move webPort handling somewhere else?
  203. bool _wsStore(const String& key, const String& value) {
  204. if (key == "webPort") {
  205. if ((value.toInt() == 0) || (value.toInt() == 80)) {
  206. return delSetting(key);
  207. }
  208. }
  209. if (value != getSetting(key)) {
  210. return setSetting(key, value);
  211. }
  212. return false;
  213. }
  214. // -----------------------------------------------------------------------------
  215. // Store indexed key (key0, key1, etc.) from array
  216. // -----------------------------------------------------------------------------
  217. bool _wsStore(const String& key, JsonArray& value) {
  218. bool changed = false;
  219. unsigned char index = 0;
  220. for (auto element : value) {
  221. if (_wsStore(key + index, element.as<String>())) changed = true;
  222. index++;
  223. }
  224. // Delete further values
  225. for (unsigned char i=index; i<SETTINGS_MAX_LIST_COUNT; i++) {
  226. if (!delSetting(key, index)) break;
  227. changed = true;
  228. }
  229. return changed;
  230. }
  231. bool _wsCheckKey(const String& key, JsonVariant& value) {
  232. for (auto& callback : _ws_callbacks.on_keycheck) {
  233. if (callback(key.c_str(), value)) return true;
  234. // TODO: remove this to call all OnKeyCheckCallbacks with the
  235. // current key/value
  236. }
  237. return false;
  238. }
  239. void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) {
  240. //DEBUG_MSG_P(PSTR("[WEBSOCKET] Parsing: %s\n"), length ? (char*) payload : "");
  241. // Get client ID
  242. uint32_t client_id = client->id();
  243. // Check early for empty object / nothing
  244. if ((length == 0) || (length == 1)) {
  245. return;
  246. }
  247. if ((length == 3) && (strcmp((char*) payload, "{}") == 0)) {
  248. return;
  249. }
  250. // Parse JSON input
  251. // TODO: json buffer should be pretty efficient with the non-const payload,
  252. // most of the space is taken by the object key references
  253. DynamicJsonBuffer jsonBuffer(512);
  254. JsonObject& root = jsonBuffer.parseObject((char *) payload);
  255. if (!root.success()) {
  256. DEBUG_MSG_P(PSTR("[WEBSOCKET] JSON parsing error\n"));
  257. wsSend_P(client_id, PSTR("{\"message\": 3}"));
  258. return;
  259. }
  260. // Check actions -----------------------------------------------------------
  261. const char* action = root["action"];
  262. if (action) {
  263. DEBUG_MSG_P(PSTR("[WEBSOCKET] Requested action: %s\n"), action);
  264. if (strcmp(action, "reboot") == 0) {
  265. deferredReset(100, CUSTOM_RESET_WEB);
  266. return;
  267. }
  268. if (strcmp(action, "reconnect") == 0) {
  269. _web_defer.once_ms(100, wifiDisconnect);
  270. return;
  271. }
  272. if (strcmp(action, "factory_reset") == 0) {
  273. DEBUG_MSG_P(PSTR("\n\nFACTORY RESET\n\n"));
  274. resetSettings();
  275. deferredReset(100, CUSTOM_RESET_FACTORY);
  276. return;
  277. }
  278. JsonObject& data = root["data"];
  279. if (data.success()) {
  280. // Callbacks
  281. for (auto& callback : _ws_callbacks.on_action) {
  282. callback(client_id, action, data);
  283. }
  284. // Restore configuration via websockets
  285. if (strcmp(action, "restore") == 0) {
  286. if (settingsRestoreJson(data)) {
  287. wsSend_P(client_id, PSTR("{\"message\": 5}"));
  288. } else {
  289. wsSend_P(client_id, PSTR("{\"message\": 4}"));
  290. }
  291. }
  292. return;
  293. }
  294. };
  295. // Check configuration -----------------------------------------------------
  296. JsonObject& config = root["config"];
  297. if (config.success()) {
  298. DEBUG_MSG_P(PSTR("[WEBSOCKET] Parsing configuration data\n"));
  299. String adminPass;
  300. bool save = false;
  301. for (auto kv: config) {
  302. bool changed = false;
  303. String key = kv.key;
  304. JsonVariant& value = kv.value;
  305. // Check password
  306. if (key == "adminPass") {
  307. if (!value.is<JsonArray&>()) continue;
  308. JsonArray& values = value.as<JsonArray&>();
  309. if (values.size() != 2) continue;
  310. if (values[0].as<String>().equals(values[1].as<String>())) {
  311. String password = values[0].as<String>();
  312. if (password.length() > 0) {
  313. setSetting(key, password);
  314. save = true;
  315. wsSend_P(client_id, PSTR("{\"action\": \"reload\"}"));
  316. }
  317. } else {
  318. wsSend_P(client_id, PSTR("{\"message\": 7}"));
  319. }
  320. continue;
  321. }
  322. if (!_wsCheckKey(key, value)) {
  323. delSetting(key);
  324. continue;
  325. }
  326. // Store values
  327. if (value.is<JsonArray&>()) {
  328. if (_wsStore(key, value.as<JsonArray&>())) changed = true;
  329. } else {
  330. if (_wsStore(key, value.as<String>())) changed = true;
  331. }
  332. // Update flags if value has changed
  333. if (changed) {
  334. save = true;
  335. }
  336. }
  337. // Save settings
  338. if (save) {
  339. // Callbacks
  340. espurnaReload();
  341. // Persist settings
  342. saveSettings();
  343. wsSend_P(client_id, PSTR("{\"message\": 8}"));
  344. } else {
  345. wsSend_P(client_id, PSTR("{\"message\": 9}"));
  346. }
  347. }
  348. }
  349. void _wsUpdate(JsonObject& root) {
  350. root["heap"] = getFreeHeap();
  351. root["uptime"] = getUptime();
  352. root["rssi"] = WiFi.RSSI();
  353. root["loadaverage"] = systemLoadAverage();
  354. #if ADC_MODE_VALUE == ADC_VCC
  355. root["vcc"] = ESP.getVcc();
  356. #endif
  357. #if NTP_SUPPORT
  358. if (ntpSynced()) root["now"] = now();
  359. #endif
  360. }
  361. void _wsDoUpdate(bool reset = false) {
  362. static unsigned long last = millis();
  363. if (reset) {
  364. last = millis() + WS_UPDATE_INTERVAL;
  365. return;
  366. }
  367. if (millis() - last > WS_UPDATE_INTERVAL) {
  368. last = millis();
  369. wsSend(_wsUpdate);
  370. }
  371. }
  372. bool _wsOnKeyCheck(const char * key, JsonVariant& value) {
  373. if (strncmp(key, "ws", 2) == 0) return true;
  374. if (strncmp(key, "admin", 5) == 0) return true;
  375. if (strncmp(key, "hostname", 8) == 0) return true;
  376. if (strncmp(key, "desc", 4) == 0) return true;
  377. if (strncmp(key, "webPort", 7) == 0) return true;
  378. return false;
  379. }
  380. void _wsOnConnected(JsonObject& root) {
  381. char chipid[7];
  382. snprintf_P(chipid, sizeof(chipid), PSTR("%06X"), ESP.getChipId());
  383. root["webMode"] = WEB_MODE_NORMAL;
  384. root["app_name"] = APP_NAME;
  385. root["app_version"] = APP_VERSION;
  386. root["app_build"] = buildTime();
  387. #if defined(APP_REVISION)
  388. root["app_revision"] = APP_REVISION;
  389. #endif
  390. root["manufacturer"] = MANUFACTURER;
  391. root["chipid"] = String(chipid);
  392. root["mac"] = WiFi.macAddress();
  393. root["bssid"] = WiFi.BSSIDstr();
  394. root["channel"] = WiFi.channel();
  395. root["device"] = DEVICE;
  396. root["hostname"] = getSetting("hostname");
  397. root["desc"] = getSetting("desc");
  398. root["network"] = getNetwork();
  399. root["deviceip"] = getIP();
  400. root["sketch_size"] = ESP.getSketchSize();
  401. root["free_size"] = ESP.getFreeSketchSpace();
  402. root["sdk"] = ESP.getSdkVersion();
  403. root["core"] = getCoreVersion();
  404. root["btnDelay"] = getSetting("btnDelay", BUTTON_DBLCLICK_DELAY).toInt();
  405. root["webPort"] = getSetting("webPort", WEB_PORT).toInt();
  406. root["wsAuth"] = getSetting("wsAuth", WS_AUTHENTICATION).toInt() == 1;
  407. #if TERMINAL_SUPPORT
  408. root["cmdVisible"] = 1;
  409. #endif
  410. root["hbMode"] = getSetting("hbMode", HEARTBEAT_MODE).toInt();
  411. root["hbInterval"] = getSetting("hbInterval", HEARTBEAT_INTERVAL).toInt();
  412. _wsDoUpdate(true);
  413. }
  414. void wsSend(JsonObject& root) {
  415. // TODO: avoid serializing twice?
  416. size_t len = root.measureLength();
  417. AsyncWebSocketMessageBuffer* buffer = _ws.makeBuffer(len);
  418. if (buffer) {
  419. root.printTo(reinterpret_cast<char*>(buffer->get()), len + 1);
  420. _ws.textAll(buffer);
  421. }
  422. }
  423. void wsSend(uint32_t client_id, JsonObject& root) {
  424. AsyncWebSocketClient* client = _ws.client(client_id);
  425. if (client == nullptr) return;
  426. // TODO: avoid serializing twice?
  427. size_t len = root.measureLength();
  428. AsyncWebSocketMessageBuffer* buffer = _ws.makeBuffer(len);
  429. if (buffer) {
  430. root.printTo(reinterpret_cast<char*>(buffer->get()), len + 1);
  431. client->text(buffer);
  432. }
  433. }
  434. void _wsConnected(uint32_t client_id) {
  435. const bool changePassword = (USE_PASSWORD && WEB_FORCE_PASS_CHANGE)
  436. ? getAdminPass().equals(ADMIN_PASS)
  437. : false;
  438. if (changePassword) {
  439. StaticJsonBuffer<JSON_OBJECT_SIZE(1)> jsonBuffer;
  440. JsonObject& root = jsonBuffer.createObject();
  441. root["webMode"] = WEB_MODE_PASSWORD;
  442. wsSend(client_id, root);
  443. return;
  444. }
  445. wsPostAll(client_id, _ws_callbacks.on_visible);
  446. wsPostSequence(client_id, _ws_callbacks.on_connected);
  447. wsPostSequence(client_id, _ws_callbacks.on_data);
  448. }
  449. void _wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){
  450. if (type == WS_EVT_CONNECT) {
  451. client->_tempObject = nullptr;
  452. #ifndef NOWSAUTH
  453. if (!_wsAuth(client)) {
  454. wsSend_P(client->id(), PSTR("{\"message\": 10}"));
  455. DEBUG_MSG_P(PSTR("[WEBSOCKET] Validation check failed\n"));
  456. client->close();
  457. return;
  458. }
  459. #endif
  460. IPAddress ip = client->remoteIP();
  461. DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u connected, ip: %d.%d.%d.%d, url: %s\n"), client->id(), ip[0], ip[1], ip[2], ip[3], server->url());
  462. _wsConnected(client->id());
  463. wifiReconnectCheck();
  464. } else if(type == WS_EVT_DISCONNECT) {
  465. DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u disconnected\n"), client->id());
  466. if (client->_tempObject) {
  467. delete (WebSocketIncommingBuffer *) client->_tempObject;
  468. }
  469. wifiReconnectCheck();
  470. } else if(type == WS_EVT_ERROR) {
  471. DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u error(%u): %s\n"), client->id(), *((uint16_t*)arg), (char*)data);
  472. } else if(type == WS_EVT_PONG) {
  473. DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u pong(%u): %s\n"), client->id(), len, len ? (char*) data : "");
  474. } else if(type == WS_EVT_DATA) {
  475. //DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u data(%u): %s\n"), client->id(), len, len ? (char*) data : "");
  476. if (!client->_tempObject) return;
  477. WebSocketIncommingBuffer *buffer = (WebSocketIncommingBuffer *)client->_tempObject;
  478. AwsFrameInfo * info = (AwsFrameInfo*)arg;
  479. buffer->data_event(client, info, data, len);
  480. }
  481. }
  482. // TODO: make this generic loop method to queue important ws messages?
  483. // or, if something uses ticker / async ctx to send messages,
  484. // it needs a retry mechanism built into the callback object
  485. void _wsHandleClientData() {
  486. if (_ws_client_data.empty()) return;
  487. auto& data = _ws_client_data.front();
  488. AsyncWebSocketClient* ws_client = _ws.client(data.client_id);
  489. if (!ws_client) {
  490. _ws_client_data.pop();
  491. return;
  492. }
  493. // wait until we can send the next batch of messages
  494. // XXX: enforce that callbacks send only one message per iteration
  495. if (ws_client->queueIsFull()) {
  496. return;
  497. }
  498. // XXX: block allocation will try to create *2 next time,
  499. // likely failing and causing wsSend to reference empty objects
  500. // XXX: arduinojson6 will not do this, but we may need to use per-callback buffers
  501. constexpr const size_t BUFFER_SIZE = 3192;
  502. DynamicJsonBuffer jsonBuffer(BUFFER_SIZE);
  503. JsonObject& root = jsonBuffer.createObject();
  504. data.send(root);
  505. if (data.client_id) {
  506. wsSend(data.client_id, root);
  507. } else {
  508. wsSend(root);
  509. }
  510. yield();
  511. if (data.done()) {
  512. // push the queue and finally allow incoming messages
  513. _ws_client_data.pop();
  514. ws_client->_tempObject = new WebSocketIncommingBuffer(_wsParse, true);
  515. }
  516. }
  517. void _wsLoop() {
  518. if (!wsConnected()) return;
  519. _wsDoUpdate();
  520. _wsHandleClientData();
  521. #if DEBUG_WEB_SUPPORT
  522. _ws_debug.send();
  523. #endif
  524. }
  525. // -----------------------------------------------------------------------------
  526. // Public API
  527. // -----------------------------------------------------------------------------
  528. bool wsConnected() {
  529. return (_ws.count() > 0);
  530. }
  531. bool wsConnected(uint32_t client_id) {
  532. return _ws.hasClient(client_id);
  533. }
  534. ws_callbacks_t& wsRegister() {
  535. return _ws_callbacks;
  536. }
  537. void wsSend(ws_on_send_callback_f callback) {
  538. if (_ws.count() > 0) {
  539. DynamicJsonBuffer jsonBuffer(512);
  540. JsonObject& root = jsonBuffer.createObject();
  541. callback(root);
  542. wsSend(root);
  543. }
  544. }
  545. void wsSend(const char * payload) {
  546. if (_ws.count() > 0) {
  547. _ws.textAll(payload);
  548. }
  549. }
  550. void wsSend_P(PGM_P payload) {
  551. if (_ws.count() > 0) {
  552. char buffer[strlen_P(payload)];
  553. strcpy_P(buffer, payload);
  554. _ws.textAll(buffer);
  555. }
  556. }
  557. void wsSend(uint32_t client_id, ws_on_send_callback_f callback) {
  558. AsyncWebSocketClient* client = _ws.client(client_id);
  559. if (client == nullptr) return;
  560. DynamicJsonBuffer jsonBuffer(512);
  561. JsonObject& root = jsonBuffer.createObject();
  562. callback(root);
  563. wsSend(client_id, root);
  564. }
  565. void wsSend(uint32_t client_id, const char * payload) {
  566. _ws.text(client_id, payload);
  567. }
  568. void wsSend_P(uint32_t client_id, PGM_P payload) {
  569. char buffer[strlen_P(payload)];
  570. strcpy_P(buffer, payload);
  571. _ws.text(client_id, buffer);
  572. }
  573. void wsPost(const ws_on_send_callback_f& cb) {
  574. _ws_client_data.emplace(cb);
  575. }
  576. void wsPostAll(uint32_t client_id, const ws_on_send_callback_list_t& cbs) {
  577. _ws_client_data.emplace(client_id, cbs, ws_data_t::ALL);
  578. }
  579. void wsPostAll(const ws_on_send_callback_list_t& cbs) {
  580. _ws_client_data.emplace(0, cbs, ws_data_t::ALL);
  581. }
  582. void wsPostSequence(uint32_t client_id, const ws_on_send_callback_list_t& cbs) {
  583. _ws_client_data.emplace(client_id, cbs, ws_data_t::SEQUENCE);
  584. }
  585. void wsPostSequence(const ws_on_send_callback_list_t& cbs) {
  586. _ws_client_data.emplace(0, cbs, ws_data_t::SEQUENCE);
  587. }
  588. void wsSetup() {
  589. _ws.onEvent(_wsEvent);
  590. webServer()->addHandler(&_ws);
  591. // CORS
  592. const String webDomain = getSetting("webDomain", WEB_REMOTE_DOMAIN);
  593. DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", webDomain);
  594. if (!webDomain.equals("*")) {
  595. DefaultHeaders::Instance().addHeader("Access-Control-Allow-Credentials", "true");
  596. }
  597. webServer()->on("/auth", HTTP_GET, _onAuth);
  598. wsRegister()
  599. .onConnected(_wsOnConnected)
  600. .onKeyCheck(_wsOnKeyCheck);
  601. espurnaRegisterLoop(_wsLoop);
  602. }
  603. #endif // WEB_SUPPORT