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}
-
@@ -1348,7 +257,7 @@
- {#if userdata}
+ {#if wsManager.userdata}
@@ -1367,16 +276,16 @@
-
+
- {#if !socketConnected && currentPageName != "/|"}
-
+ {#if !wsManager.socketConnected && wsManager.currentPageName != "/|"}
+
{:else}
- wsPush(ws, topic, status)} />
+ wsManager.wsPush(ws, topic, status)} />
-
+
{/if}
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();