diff --git a/rollup.config.js b/rollup.config.js index 2e43462..a832614 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -49,7 +49,7 @@ export default { module: true, toplevel: true, unsafe_arrows: true, - drop_console: true, + drop_console: false, drop_debugger: true, }, output: { quote_style: 1 }, diff --git a/src/App.svelte b/src/App.svelte index db1226d..615b8d1 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -1,103 +1,32 @@ @@ -1306,7 +215,7 @@
{#if showDropdown}
- devicesDropdownChange()}> {#each deviceList as device}
@@ -1348,7 +257,7 @@
  • {"Устройства"}
  • - {#if userdata} + {#if wsManager.userdata}
  • {"Модули"}
  • @@ -1367,16 +276,16 @@ -
    +
    diff --git a/src/WebSocketManager.js b/src/WebSocketManager.js new file mode 100644 index 0000000..a5427c4 --- /dev/null +++ b/src/WebSocketManager.js @@ -0,0 +1,572 @@ +class WebSocketManager { + constructor(deviceList, debug = true) { + this.deviceList = deviceList; + this.debug = debug; + this.socket = []; + this.selectedWs = 0; + this.currentPageName = undefined; + this.reconnectTimeout = 60; + this.remainingTimeout = this.reconnectTimeout; + this.preventReconnect = false; + this.waitingAckTimeout = 18000; + this.rebootOrUpdateProcess = false; + this.showAwaitingCircle = false; + this.socketConnected = false; + this.ackTimeoutsArr = []; + this.startMillis = []; + this.ping = []; + this.layoutJson = []; + this.paramsJson = {}; + this.itemsJson = {}; + this.widgetsJson = {}; + this.configJson = {}; + this.scenarioTxt = ""; + this.settingsJson = {}; + this.ssidJson = {}; + this.errorsJson = {}; + this.incDeviceList = []; + this.flashProfileJson = {}; + this.otaJson = {}; + this.firstDevListRequest = true; + this.parsed = this.initializeParsedFlags(); + this.pageReady = this.initializePageReadyFlags(); + this.pages = []; + } + + initializeParsedFlags() { + return { + itemsJson: false, + widgetsJson: false, + configJson: false, + settingsJson: false, + ssidJson: false, + errorsJson: false, + incDeviceList: false, + flashProfileJson: false, + otaJson: false, + }; + } + + initializePageReadyFlags() { + return { + dash: false, + config: false, + connection: false, + list: false, + system: false, + dev: false, + }; + } + + connectToAllDevices() { + this.deviceList.forEach((device, i) => { + device.ws = i; + if (!device.status) { + this.wsConnect(i); + this.wsEventAdd(i); + } + }); + } + + async initDevList() { + if (this.firstDevListRequest) { + this.devListOverride(); + } else { + this.devListCombine(); + } + this.firstDevListRequest = false; + this.parsed.deviceListJson = true; + this.debug && console.log("[✔]", "deviceList parsed"); + this.onParsed(); + this.selectedDeviceDataRefresh(); + this.connectToAllDevices(); + } + + wsConnect(ws) { + const ip = this.getIP(ws); + if (ip === "error") { + this.debug && console.log("[e]", "device list wrong"); + } else { + this.socket[ws] = new WebSocket(`ws://${ip}:81`); + this.socket[ws].binaryType = "blob"; + this.debug && console.log("[i]", ip, ws, "started connecting..."); + } + } + + getIP(ws) { + const device = this.deviceList.find((device) => device.ws === ws); + return device ? device.ip : "error"; + } + + wsEventAdd(ws) { + if (!this.socket[ws]) { + this.debug && console.log("[e]", "socket not exist"); + return; + } + + const ip = this.getIP(ws); + this.socket[ws].addEventListener("open", () => this.handleOpen(ws, ip)); + this.socket[ws].addEventListener("message", (event) => this.handleMessage(event, ws)); + this.socket[ws].addEventListener("close", () => this.handleClose(ws, ip)); + this.socket[ws].addEventListener("error", () => this.handleError(ws, ip)); + } + + handleOpen(ws, ip) { + this.markDeviceStatus(ws, true); + if (this.firstDevListRequest && ws === 0) this.wsSendMsg(ws, "/devlist|"); + if (this.currentPageName === "/|") { + this.wsSendMsg(ws, this.currentPageName); + } else if (ws === this.selectedWs) { + this.sendCurrentPageNameToSelectedWs(); + } + } + + handleMessage(event, ws) { + if (typeof event.data === "string" && event.data === "/tstr|") { + this.ack(ws, true); + } else if (event.data instanceof Blob) { + if (ws === this.selectedWs) { + this.parseBlob(event.data, ws); + } + if (this.currentPageName === "/|") { + this.parseAllBlob(event.data, ws); + } + } + } + + handleClose(ws, ip) { + this.debug && console.log("[e]", ip, "connection closed"); + this.markDeviceStatus(ws, false); + } + + handleError(ws, ip) { + this.debug && console.log("[e]", ip, "connection error"); + this.markDeviceStatus(ws, false); + } + + markDeviceStatus(ws, status) { + this.deviceList.forEach((device) => { + if (device.ws === ws) { + device.status = status; + device.ping = 0; + this.debug && console.log("[i]", device.ip, ws, `status ${status ? "online" : "offline"}`); + if (!status) { + this.deleteWidget(ws); + this.sortingLayout(ws); + } + } + }); + this.selectedDeviceDataRefresh(); + } + + deleteWidget(ws) { + this.layoutJson = this.layoutJson.filter((item) => item.ws !== ws); + } + + async parseBlob(blob, ws) { + const header = await blob.slice(0, 6).text(); + const size = await blob.slice(7, 11).text(); + + const handlers = { + itemsj: this.handleItemsJson, + widget: this.handleWidgetsJson, + config: this.handleConfigJson, + scenar: this.handleScenarioTxt, + settin: this.handleSettingsJson, + ssidli: this.handleSsidJson, + errors: this.handleErrorsJson, + devlis: this.handleDeviceList, + prfile: this.handleFlashProfileJson, + otaupd: this.handleOtaJson, + corelg: this.handleCoreLog, + }; + + if (handlers[header]) { + await handlers[header].call(this, blob, size, ws); + } + + await this.onParsed(); + } + + async handleItemsJson(blob, size) { + await this.parseJsonPayload(blob, size, "itemsJson"); + } + + async handleWidgetsJson(blob, size) { + await this.parseJsonPayload(blob, size, "widgetsJson"); + } + + async handleConfigJson(blob, size) { + await this.parseJsonPayload(blob, size, "configJson"); + } + + async handleScenarioTxt(blob, size) { + this.scenarioTxt = await this.getPayloadAsTxt(blob, size); + this.debug && console.log("[i]", "scenarioTxt: ", this.scenarioTxt); + } + + async handleSettingsJson(blob, size) { + await this.parseJsonPayload(blob, size, "settingsJson"); + } + + async handleSsidJson(blob, size) { + await this.parseJsonPayload(blob, size, "ssidJson"); + } + + async handleErrorsJson(blob, size) { + await this.parseJsonPayload(blob, size, "errorsJson"); + } + + async handleDeviceList(blob, size, ws) { + await this.parseJsonPayload(blob, size, "incDeviceList"); + await this.initDevList(); + } + + async handleFlashProfileJson(blob, size) { + await this.parseJsonPayload(blob, size, "flashProfileJson"); + } + + async handleOtaJson(blob, size) { + await this.parseJsonPayload(blob, size, "otaJson"); + } + + async handleCoreLog(blob, size) { + const txt = await this.getPayloadAsTxt(blob, size); + this.addCoreMsg(txt); + } + + async parseJsonPayload(blob, size, jsonKey) { + const out = {}; + if (await this.getPayloadAsJson(blob, size, out)) { + this[jsonKey] = out.json; + this.parsed[jsonKey] = true; + this.debug && console.log("[✔]", `${jsonKey}: `, this[jsonKey]); + } else { + this.parsed[jsonKey] = false; + this.debug && console.log("[e]", `${jsonKey} parse error`); + } + } + + async parseAllBlob(blob, ws) { + const header = await blob.slice(0, 6).text(); + const size = await blob.slice(7, 11).text(); + + const handlers = { + status: this.handleStatusJson, + layout: this.handleLayoutJson, + params: this.handleParamsJson, + charta: this.handleChartAJson, + chartb: this.handleChartBJson, + }; + + if (handlers[header]) { + await handlers[header].call(this, blob, size, ws); + } + } + + async handleStatusJson(blob, size) { + await this.parseJsonPayload(blob, size, "statusJson"); + } + + async handleLayoutJson(blob, size, ws) { + const out = {}; + if (await this.getPayloadAsJson(blob, size, out)) { + this.combineLayoutsInOne(ws, out.json); + this.debug && console.log("[✔]", "devLayout: ", out.json); + } else { + this.debug && console.log("[e]", "devLayout parse error"); + } + } + + async handleParamsJson(blob, size, ws) { + const out = {}; + if (await this.getPayloadAsJson(blob, size, out)) { + this.paramsJson = { ...this.paramsJson, ...out.json }; + this.updateAllStatuses(ws); + this.onParsed(); + this.debug && console.log("[✔]", "devParams: ", out.json); + } else { + this.debug && console.log("[e]", "devParams parse error"); + } + } + + async handleChartAJson(blob, size) { + const txt = await this.getPayloadAsTxt(blob, size); + const chartJson = JSON.parse(`[${txt.substring(0, txt.length - 1)}]`); + const out = {}; + if (await this.getJsonAsJson(blob, size, out)) { + const finalDataJson = { status: chartJson, ...out.json }; + this.updateWidgetByArray(finalDataJson); + this.debug && console.log("[✔]", "chartJson: ", finalDataJson); + } else { + this.debug && console.log("[e]", "chart json add-ns parse error"); + } + } + + async handleChartBJson(blob, size) { + await this.parseJsonPayload(blob, size, "chartStatus"); + } + + sendCurrentPageNameToSelectedWs() { + if (this.selectedWs !== undefined) { + this.wsSendMsg(this.selectedWs, this.currentPageName); + } + } + + wsSendMsg(ws, msg) { + if (this.socket[ws]?.readyState === 1) { + this.socket[ws].send(msg); + this.debug && console.log("[i]", this.getIP(ws), ws, "msg send success", msg); + } else { + this.debug && console.log("[e]", this.getIP(ws), ws, "msg not send"); + } + } + + ack(ws, st) { + if (!st) { + this.startMillis[ws] = Date.now(); + this.ackTimeoutsArr[ws] = setTimeout(() => { + this.markDeviceStatus(ws, false); + }, this.waitingAckTimeout); + } else { + clearTimeout(this.ackTimeoutsArr[ws]); + this.ping[ws] = Date.now() - this.startMillis[ws]; + this.deviceList.forEach((device) => { + if (device.ws === ws) { + device.ping = this.ping[ws]; + } + }); + } + } + + selectedDeviceDataRefresh() { + const selectedDevice = this.deviceList.find((device) => device.ws === this.selectedWs); + this.socketConnected = selectedDevice?.status || false; + } + + sortingLayout(ws) { + this.layoutJson.sort((a, b) => a.descr.localeCompare(b.descr)); + this.wsSendMsg(ws, "/params|"); + } + + wsTestMsgTask() { + setTimeout(() => this.wsTestMsgTask(), 1000); + if (!this.preventReconnect) { + this.remainingTimeout--; + if (this.rebootOrUpdateProcess && this.socketConnected) { + this.rebootOrUpdateProcess = false; + this.showAwaitingCircle = false; + this.reconnectTimeout = 60; + this.remainingTimeout = this.reconnectTimeout; + } + if (this.remainingTimeout <= 0) { + this.debug && console.log("[i]", "----timer tick----"); + this.remainingTimeout = this.reconnectTimeout; + this.deviceList.forEach((device) => { + if (!device.status) { + this.wsConnect(device.ws); + this.wsEventAdd(device.ws); + } else { + this.wsSendMsg(device.ws, "/tst|"); + this.ack(device.ws, false); + } + }); + } + } + } + + async getPayloadAsJson(blob, size, out) { + const partBlob = blob.slice(size, blob.length); + const txt = await partBlob.text(); + try { + out.json = JSON.parse(txt); + out.parse = true; + } catch (e) { + this.debug && console.log("[e]", "json parse error: ", txt); + out.parse = false; + } + return out.parse; + } + + async getPayloadAsTxt(blob, size) { + const partBlob = blob.slice(size, blob.length); + return await partBlob.text(); + } + + async getJsonAsJson(blob, size, out) { + const partBlob = blob.slice(size, blob.length); + const txt = await partBlob.text(); + try { + out.json = JSON.parse(txt); + out.parse = true; + } catch (e) { + this.debug && console.log("[e]", "json parse error: ", txt); + out.parse = false; + } + return out.parse; + } + + async devListOverride() { + this.deviceList = this.incDeviceList; + this.sortList(this.deviceList); + this.deviceList[0].status = true; + this.debug && console.log("[i]", "[devlist]", "devlist overridden"); + } + + async onParsed() { + const pageHandlers = { + "/|": () => (this.pageReady.dash = true), + "/config|": () => this.handleConfigPage(), + "/connection|": () => this.handleConnectionPage(), + "/list|": () => this.handleListPage(), + "/system|": () => this.handleSystemPage(), + "/profile|": () => this.handleProfilePage(), + }; + + if (pageHandlers[this.currentPageName]) { + await pageHandlers[this.currentPageName].call(this); + } + } + + handleConfigPage() { + if (this.parsed.itemsJson && this.parsed.widgetsJson && this.parsed.configJson && this.parsed.settingsJson) { + this.clearParsedFlags(); + this.pageReady.config = true; + this.debug && console.log("✔✔", "config page parsed"); + } + } + + handleConnectionPage() { + if (this.parsed.ssidJson && this.parsed.settingsJson && this.parsed.errorsJson) { + this.clearParsedFlags(); + this.pageReady.connection = true; + this.debug && console.log("✔✔", "connection page parsed"); + } + } + + handleListPage() { + if (this.parsed.settingsJson) { + this.clearParsedFlags(); + this.pageReady.list = true; + this.debug && console.log("✔✔", "list page parsed"); + } + } + + handleSystemPage() { + if (this.parsed.errorsJson && this.parsed.settingsJson) { + this.clearParsedFlags(); + this.getVersionsList(); + this.pageReady.system = true; + this.debug && console.log("✔✔", "system page parsed"); + } + } + + async handleProfilePage() { + if (this.parsed.flashProfileJson) { + this.clearParsedFlags(); + this.pageReady.profile = true; + this.debug && console.log("✔✔", "profile page parsed"); + await this.getModInfo(); + await this.getProfile(); + } + } + + async devListCombine() { + this.deviceList = this.combineArrays(this.deviceList, this.incDeviceList); + this.sortList(this.deviceList); + this.debug && console.log("[i]", "[devlist]", "devlist combined"); + } + + combineArrays(A, B) { + const ids = new Set(A.map((d) => d.ip)); + return [...A, ...B.filter((d) => !ids.has(d.ip))]; + } + + getSelectedDeviceData(ws) { + this.selectedDeviceData = this.deviceList.find((device) => device.ws === ws); + } + + sortList(list) { + const firstDev = list.shift(); + list.sort((a, b) => a.name.localeCompare(b.name)); + list.unshift(firstDev); + } + + sendToAllDevices(msg) { + this.deviceList.forEach((device) => { + if (device.status === true) { + this.wsSendMsg(device.ws, msg); + } + }); + } + + //слияние layout-ов всех устройств в общий layout + async combineLayoutsInOne(ws, devLayout) { + for (let i = 0; i < devLayout.length; i++) { + devLayout[i].ws = ws; + } + this.layoutJson = this.layoutJson.concat(devLayout); + console.log("[2]", ws, "devLayout pushed to layout"); + this.sortingLayout(ws); + } + + updateAllStatuses(ws) { + for (const [key, value] of Object.entries(this.paramsJson)) { + for (let i = 0; i < this.layoutJson.length; i++) { + let topic = this.layoutJson[i].topic; + if (topic) { + //this.layoutJson[i].ws = ws; + topic = topic.substring(topic.lastIndexOf("/") + 1, topic.length); + if (key === topic) { + console.log("[i]", "updated =>" + topic, value); + this.layoutJson[i].status = value; + break; + } + } + } + } + this.wsSendMsg(ws, "/charts|"); + } + + sortingLayout(ws) { + //сортируем весь layout по алфавиту + this.layoutJson.sort(function (a, b) { + if (a.descr < b.descr) { + return -1; + } + if (a.descr > b.descr) { + return 1; + } + return 0; + }); + //формируем json всех карточек + this.pages = []; + const newPage = Array.from(new Set(Array.from(this.layoutJson, ({ page }) => page))); + newPage.forEach(function (item, i, arr) { + this.pages = [ + ...this.pages, + JSON.parse( + JSON.stringify({ + page: item, + }) + ), + ]; + }); + //сортируем карточки по алфавиту + this.pages.sort(function (a, b) { + if (a.page < b.page) { + return -1; + } + if (a.page > b.page) { + return 1; + } + return 0; + }); + + this.layoutJson = this.layoutJson; + console.log("[3]", ws, "layout sort, requested params..."); + this.wsSendMsg(ws, "/params|"); + } +} + +export default WebSocketManager; diff --git a/src/pages/Dashboard.svelte b/src/pages/Dashboard.svelte index 0fcaec1..ee4873a 100644 --- a/src/pages/Dashboard.svelte +++ b/src/pages/Dashboard.svelte @@ -7,7 +7,7 @@ import Anydata from "../widgets/Anydata.svelte"; import Alarm from "../components/Alarm.svelte"; - export let layoutJson; + export let layoutJson = []; $: layoutJson.length, timeOut();