diff --git a/.cursor/plans/IoTManagerWeb.code-workspace b/.cursor/plans/IoTManagerWeb.code-workspace
index 0aed4be..e280649 100644
--- a/.cursor/plans/IoTManagerWeb.code-workspace
+++ b/.cursor/plans/IoTManagerWeb.code-workspace
@@ -5,6 +5,9 @@
},
{
"path": "../../../IoTManager"
+ },
+ {
+ "path": "../../../IoTManagerWeb2"
}
],
"settings": {}
diff --git a/.cursor/plans/websocketmanager_refactor_d29d54ab.plan.md b/.cursor/plans/websocketmanager_refactor_d29d54ab.plan.md
new file mode 100644
index 0000000..f6573f4
--- /dev/null
+++ b/.cursor/plans/websocketmanager_refactor_d29d54ab.plan.md
@@ -0,0 +1,174 @@
+---
+name: WebSocketManager refactor
+overview: "Привести IoTManagerWeb к архитектуре по аналогии с Web2: один класс WebSocketManager как основа, EventEmitter для уведомления UI, App.svelte — тонкий слой роутинга и разметки. Без копирования кода из Web2, с опорой на существующие api/ и lib/."
+todos: []
+isProject: false
+---
+
+# Рефакторинг: WebSocketManager как основа (по аналогии с Web2)
+
+## Важно: что не терять
+
+**Web2 — только архитектурный пример (структура).** Код из Web2 не копируем: там есть ошибки и отличия протокола. Пишем новый код, сохраняя всю рабочую логику текущего IoTManagerWeb.
+
+**Обязательно сохранить:**
+
+1. **Получение списка устройств и подключение к ним**
+ - Цепочка: один начальный deviceList (первое устройство) → open(ws=0) → при `firstDevListRequest` отправка `/devlist|` → приход blob с заголовком `devlis` → `initDevList` (devListOverride при первом запросе, devListCombine при последующих) → обновление deviceList → `connectToAllDevices()` для всех из списка. Не менять порядок и условия (в т.ч. deviceList[0].status = true в devListOverride).
+2. **Все страницы должны работать** — Управление, Конфигуратор, Подключение, Системные, Устройства, Вход (и при авторизации — Модули/Profile). Механизмы по страницам:
+ - **Управление (Dashboard)** — рассылка запроса всем устройствам при `/|`, приём layout/params/charta/chartb от всех, combineLayoutsInOne, sortingLayout, updateAllStatuses, отправка `/params|` после layout и `/charts|` после params; wsPush → `/control|key/status`.
+ - **Конфигуратор** — запрос по currentPageName `/config|`, парсинг itemsj/widget/config/scenar/settin, pageReady при полном наборе; saveConfig → `/tuoyal|`, `/gifnoc|`, `/oiranecs|`; экспорт/импорт (mark iotm, scenario=>); moduleOrder → `/order|`.
+ - **Подключение** — ssidJson, settingsJson, errorsJson; saveSett/saveMqtt → `/sgnittes|`, `/mqtt|`; ssidClick → `/scan|`.
+ - **Системные** — errorsJson, settingsJson; getVersionsList (ver.json); cleanLogs → `/clean|`; rebootEsp → `/reboot|`; cancelAlarm → `/rorre|`.
+ - **Устройства (List)** — deviceList, saveList → `/tsil|`, addDevInList, devListOverride/devListCombine, udps, sendToAllDevices.
+ - **Вход (Login)** — без изменений; портал по api/portal.
+ - **Модули (Profile)** — при авторизации; flashProfileJson, getModInfo, getProfile, updateBuild и т.д.
+3. **Логика получения данных графиков**
+ - После прихода **layout** (parseAllBlob): combineLayoutsInOne → sortingLayout(ws) → отправка `**/params|**`.
+ - После прихода **params**: merge в paramsJson, updateAllStatuses(ws) (подстановка в layoutJson по topic), затем отправка `**/charts|**`.
+ - Приход **charta** / **chartb** → обновление layoutJson по topic через **apdateWidgetByArray**. Формат charta: два фрагмента (12..size — JSON addJson, size..length — текст массива точек), костыль `"[" + txt.substring(0, txt.length - 1) + "]"` не трогать. chartb — один JSON с topic и status. Всё как в текущем [lib/blobProtocol.js](src/lib/blobProtocol.js) и App.svelte.
+4. **Обновление данных виджетов (особое внимание)**
+ Виджеты на Dashboard (input, toggle, range, anydata, chart) получают данные из **layoutJson**: каждый элемент — один виджет с полями topic, status, page, widget и т.д. Данные прилетают по WebSocket в parseAllBlob (только при currentPageName === "/|") и должны сразу отражаться в UI.
+ - **Цепочка по типам blob:**
+ - **status** → **updateWidget(newStatusJson)** — найти в layoutJson элемент с совпадающим topic, слить данные **jsonConcat(layoutJson[i], newStatusJson)**, выставить **layoutJson[i].sent = false** (снять подсветку «ожидание ответа»). Один виджет обновляется по одному прилетевшему статусу.
+ - **params** → после merge в paramsJson вызывается **updateAllStatuses(ws)** — для каждого ключа из paramsJson найти виджет по topic (последний сегмент topic) и записать **layoutJson[i].status = value** (sent в updateAllStatuses не менять — это массовая подстановка из params, не ответ на действие пользователя); затем отправить `/charts|`.
+ - **charta** / **chartb** → **apdateWidgetByArray(newStatusJson)** — найти виджет по topic; **jsonConcatEx** (слить всё кроме status); для **status** либо слить массивы (**prevArr = [...prevArr, ...newArr]**), либо записать newArr; **layoutJson[i].sent = false**. Так накапливаются точки графиков по topic.
+ - **Вспомогательные функции не терять:** **jsonConcat** (полное слияние объектов), **jsonConcatEx** (слияние без поля status — т.к. status у chart накопительный).
+ - **Реактивность UI:** после любого изменения layoutJson (combineLayoutsInOne, sortingLayout, updateWidget, updateAllStatuses, apdateWidgetByArray) UI должен обновиться. В текущем App layoutJson — одна переменная, мутация layoutJson[i] или layoutJson = layoutJson триггерит Svelte. В схеме с WebSocketManager: менеджер держит this.layoutJson и после каждого такого обновления вызывать **eventEmitter.emit("layoutJsonUpdated", { layoutJson: this.layoutJson, pages: this.pages })** (в т.ч. после updateWidget, updateAllStatuses, apdateWidgetByArray), чтобы App подставил layoutJson в локальную переменную и Dashboard перерисовался. Иначе виджеты не покажут прилетевшие данные.
+5. **Обратная связь в виджетах (красная подсветка «ожидание ответа»)**
+ У каждого виджета в layoutJson есть поле **sent**. Пока **sent === true**, виджет подсвечивается красным (ожидание ответа от устройства); после прихода ответа по этому виджету **sent** сбрасывается в **false** — красный гаснет.
+ - **Где ставится sent = true:** в виджетах при действии пользователя, сразу перед отправкой команды на устройство. [Input.svelte](src/widgets/Input.svelte): при on:change делают `widget.sent = true` и вызывают wsPush(ws, topic, status). [Toggle.svelte](src/widgets/Toggle.svelte): в changeValue() перед сменой widget.status выставляют `widget.sent = true`, затем wsPush в on:change. [Range.svelte](src/widgets/Range.svelte): в on:change `(widget.sent = true), wsPush(...)`. Визуально: Input — класс `border-red-500` при widget.sent, иначе `focus:border-indigo-500`; Toggle/Range — класс `bg-red-300` у «точки»/слайдера при widget.sent, иначе серый.
+ - **Где ставится sent = false:** только при приходе данных с устройства по этому виджету. В [App.svelte](src/App.svelte): в **updateWidget** (blob с заголовком status) — после нахождения виджета по topic и jsonConcat делают `layoutJson[i].sent = false`. В **apdateWidgetByArray** (charta/chartb) — после обновления виджета по topic делают `layoutJson[i].sent = false`. В **updateAllStatuses** sent не меняется (там массовая подстановка из params перед запросом графиков, не ответ на действие пользователя).
+ - **Цепочка:** пользователь меняет значение → виджет ставит widget.sent = true → отправляется /control|key/status → устройство обрабатывает и присылает blob (status или данные графика) → parseAllBlob вызывает updateWidget или apdateWidgetByArray → по topic находят layoutJson[i] → **layoutJson[i].sent = false** → красный гаснет. При переносе логики в WebSocketManager поле **sent** должно по-прежнему жить на элементах this.layoutJson; сброс sent = false — только в updateWidget и apdateWidgetByArray; после них — emit layoutJsonUpdated, чтобы UI обновил подсветку.
+
+**Источник правды по логике и протоколу:** текущий IoTManagerWeb (и существующие api/, lib/), не Web2.
+
+**При трудностях:** всегда можно смотреть код бэкенда — [IoTManager](file:///Users/dmitry/Documents/Database/IoTManagerProject/personal/IoTManager) (протокол WS, команды, форматы blob, модули логирования/графиков: `src/WsServer.cpp`, `src/modules/virtual/Loging*` и т.д.).
+
+---
+
+## Цель
+
+- **Один менеджер** — класс `WebSocketManager` владеет состоянием устройств/WS/blob и оркестрирует **существующие** `api/` и `lib/` (без переписывания их логики).
+- **EventEmitter** — уведомление App об изменении данных (layoutJson, deviceList) без жёсткой связи с Svelte.
+- **Тонкий App** — роутинг, вызовы `wsManager.*`, подписка на события, разметка; минимум локального state.
+
+## Текущее состояние
+
+- [App.svelte](src/App.svelte): ~1226 строк, десятки `let` (deviceList, layoutJson, parsed, pageReady, selectedWs, ...), вызовы deviceConnection, blobProtocol, deviceSocket, wsReconnect.
+- [api/deviceSocket.js](src/api/deviceSocket.js): createConnection(wsIndex, ip, callbacks), send(wsIndex, msg).
+- [lib/deviceConnection.js](src/lib/deviceConnection.js): getIP, connectToAllDevices, createOpenHandler, handleDevListReceived.
+- [lib/deviceListManager.js](src/lib/deviceListManager.js): initDevList, devListOverride, devListCombine, sortList, combineArrays.
+- [lib/blobProtocol.js](src/lib/blobProtocol.js): parseBlob(blob, ws, handlers), parseAllBlob(blob, ws, handlers).
+- [lib/wsReconnect.js](src/lib/wsReconnect.js): wsTestMsgTask, ack (heartbeat, таймауты).
+
+## API страниц (контракт App → страницы)
+
+При рефакторинге передавать страницам те же props; источник данных — wsManager или реактивные переменные (layoutJson, pages, deviceList), синхронизированные с менеджером через события.
+
+
+| Страница | Данные (props) | Колбэки (props) | Связки (bind) |
+| --------------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------- |
+| **Dashboard** [Dashboard.svelte](src/pages/Dashboard.svelte) | show, layoutJson, pages | wsPush(ws, topic, status) | — |
+| **Config** [Config.svelte](src/pages/Config.svelte) | show, widgetsJson, itemsJson, userdata | saveConfig(), cleanLogs(), rebootEsp(), moduleOrder(id, key, value) | configJson, scenarioTxt |
+| **Connection** [Connection.svelte](src/pages/Connection.svelte) | show, settingsJson, errorsJson, ssidJson | rebootEsp(), ssidClick(), saveSett(), saveMqtt() | — |
+| **List** [List.svelte](src/pages/List.svelte) | show, deviceList, settingsJson, showInput, newDevice, percent | saveSett(), rebootEsp(), addDevInList(), sendToAllDevices(msg), saveList(), devListOverride(), applicationReboot() | — |
+| **System** [System.svelte](src/pages/System.svelte) | show, errorsJson, settingsJson, coreMessages, versionsList | saveSett(), rebootEsp(), cleanLogs(), cancelAlarm(alarmKey) | choosingVersion |
+| **Profile** [Profile.svelte](src/pages/Profile.svelte) | show, flashProfileJson, userdata, allmodeinfo, profile, serverOnline, otaJson | updateBuild(path) | — |
+| **Login** [Login.svelte](src/pages/Login.svelte) | show, serverOnline | — | — |
+
+
+**Детали по страницам:**
+
+- **Dashboard:** layoutJson и pages — из реактивных переменных (обновляются по event layoutJsonUpdated). wsPush вызывает отправку /control|key/status для выбранного ws.
+- **Config:** configJson и scenarioTxt — bind, значит App/менеджер отдаёт объект, страница может мутировать; saveConfig отправляет layout, config, scenario; moduleOrder — /order|.
+- **Connection:** только данные и колбэки; без bind.
+- **List:** deviceList — из реактивной переменной (deviceListUpdated); newDevice — объект для формы добавления устройства; devListOverride передаётся (резерв).
+- **System:** versionsList и choosingVersion (bind) приходят из App; cancelAlarm вызывается при закрытии аларма по ключу.
+- **Profile:** все данные только на чтение; updateBuild(path) — отправка /update|path.
+- **Login:** только show и serverOnline; логика входа и сохранение JWT внутри страницы (fetch через api/portal).
+
+При переносе на WebSocketManager: show брать из wsManager.pageReady.*; данные — из wsManager.* или из локальных layoutJson/pages/deviceList, синхронизированных по событиям; колбэки — обёртки в App, вызывающие wsManager.wsSendMsg / wsManager.saveConfig и т.д.
+
+## Проверки после серьёзных изменений
+
+После каждого из отмеченных ниже шагов **останавливать выполнение плана** и просить вас:
+
+- открыть приложение в браузере;
+- проверить, что страница открывается и данные прилетают (какие именно — указано в шаге).
+
+Продолжать следующий шаг только после вашего подтверждения, что проверка пройдена.
+
+---
+
+## Шаги
+
+### 1. EventEmitter
+
+- Добавить `**src/eventEmitter.js**` (или в корень, по аналогии с Web2): класс с методами `on(event, listener)`, `off(event, listener)`, `emit(event, ...args)`. Экспорт синглтона `eventEmitter`.
+- События для начала: `**layoutJsonUpdated**` (payload: `{ layoutJson, pages }`), `**deviceListUpdated**` (чтобы App подписался и обновил локальные переменные для реактивности Svelte).
+
+### 2. Класс WebSocketManager (каркас + делегирование)
+
+- Добавить `**src/WebSocketManager.js**` (или `src/lib/WebSocketManager.js`).
+
+**Состояние (поля класса):**
+deviceList, layoutJson, paramsJson, pages, parsed, pageReady, firstDevListRequest, currentPageName, selectedWs, selectedDeviceData, socketConnected, originalWs; itemsJson, widgetsJson, configJson, scenarioTxt, settingsJson, ssidJson, errorsJson, incDeviceList, flashProfileJson, otaJson; newDevice, coreMessages; reconnectTimeout, remainingTimeout, percent, preventReconnect, rebootOrUpdateProcess, showAwaitingCircle; ack state (ackTimeoutsArr, startMillis, ping). Опционально: userdata, serverOnline, allmodeinfo, profile, versionsList, choosingVersion — либо в менеджере, либо оставить только в App и передавать в страницы из App (решение: в менеджере хранить, чтобы страницы получали всё через один объект).
+
+**Инициализация:**
+Constructor(initialDeviceList, options). Копировать initialDeviceList в this.deviceList, инициализировать parsed/pageReady объектами (как сейчас в App), firstDevListRequest = true, selectedWs = 0, currentPageName и т.д. Опции: debug.
+
+**Делегирование в существующие модули:**
+
+- **getIP(ws)** → `deviceConnection.getIP(ws, this.deviceList)`.
+- **connectToAllDevices()** → вызвать `deviceConnection.connectToAllDevices(this.deviceList, () => this.getSelectedDeviceData(this.selectedWs), this.selectedWs, (wsIndex, ip) => this._createConnection(wsIndex, ip))`. Внутри менеджера реализовать **_createConnection(wsIndex, ip)**:
+ - Вызов `deviceSocket.createConnection(wsIndex, ip, { onOpen, onMessage, onClose, onError })`.
+ - onOpen: при срабатывании события open вызывать createOpenHandler **в этот момент** (чтобы в замыкание попали актуальные this.firstDevListRequest, this.currentPageName, this.selectedWs), затем вызвать полученную функцию с (ws). То есть внутри колбэка onOpen: `const fn = deviceConnection.createOpenHandler({ markDeviceStatus: ..., sendMsg: ..., firstDevListRequest: this.firstDevListRequest, currentPageName: this.currentPageName, selectedWs: this.selectedWs, sendCurrentPageNameToSelectedWs: () => this.sendCurrentPageNameToSelectedWs() }); fn(ws);`
+ - onMessage(ws, data): если data === "/tstr|" — this.ack(ws, true); если Blob — this.parseBlob(data, ws) при ws === selectedWs, и при currentPageName === "/|" this.parseAllBlob(data, ws).
+ - onClose/onError: this.markDeviceStatus(ws, false).
+- **parseBlob / parseAllBlob** — передать в `blobProtocol.parseBlob` / `parseAllBlob` объекты handlers, где каждый setter обновляет `this.*` (this.itemsJson = v, this.parsed.itemsJson = v, ...). Для devlis: вызывать `deviceConnection.handleDevListReceived(incDeviceList, this.deviceList, this.firstDevListRequest, { setDeviceList: (list) => { this.deviceList = list; eventEmitter.emit("deviceListUpdated"); }, setFirstDevListRequest: (v) => ..., onParced: () => this.onParced(), selectedDeviceDataRefresh: () => this.selectedDeviceDataRefresh(), connectToAllDevices: () => this.connectToAllDevices() })`. В конце parseBlob вызывать this.onParced(). Для allBlobHandlers: updateWidget, combineLayoutsInOne, mergeParams, updateAllStatuses, apdateWidgetByArray — методы менеджера, которые обновляют this.layoutJson/paramsJson и в нужных местах вызывают **eventEmitter.emit("layoutJsonUpdated", { layoutJson: this.layoutJson, pages: this.pages })**.
+- **markDeviceStatus(ws, status)** — обновить deviceList, при status === false вызвать deleteWidget(ws), sortingLayout(ws), selectedDeviceDataRefresh(); затем **eventEmitter.emit("deviceListUpdated")** (и при изменении layout — уже внутри sortingLayout emit layoutJsonUpdated).
+- **updateWidget**, **updateAllStatuses**, **apdateWidgetByArray** — логика как в текущем App (поиск по topic, jsonConcat/jsonConcatEx, слияние массивов status для графиков). **sent = false** — только в updateWidget и apdateWidgetByArray; в updateAllStatuses не трогать. **После каждого из этих вызовов** вызывать **eventEmitter.emit("layoutJsonUpdated", { layoutJson: this.layoutJson, pages: this.pages })** — иначе данные виджетов прилетят в менеджер, но UI не обновится.
+- **wsSendMsg(ws, msg)** → `deviceSocket.send(ws, msg)`.
+- **ack**, **wsTestMsgTask** — либо перенести логику из wsReconnect в менеджер (как в Web2), либо оставить вызов `wsReconnect.start(this.deviceList, (ws, msg) => deviceSocket.send(ws, msg), (ws, st) => this.markDeviceStatus(ws, st), ...)` в onMount и не дублировать код. Предпочтительно: вызывать wsReconnect из менеджера (manager.startReconnectTask()), чтобы вся инициализация была в одном месте.
+
+**Методы менеджера, которые сейчас в App и переезжают в класс:**
+deleteWidget, combineLayoutsInOne, sortingLayout, updateAllStatuses, updateWidget, apdateWidgetByArray, **jsonConcat**, **jsonConcatEx** (оба нужны для обновления виджетов), clearData, clearParcedFlags, sendToAllDevices, sendCurrentPageNameToSelectedWs, getSelectedDeviceData, selectedDeviceDataRefresh, onParced (полностью с логикой по currentPageName и parsed).
+Сохранить протокол и форматы (charta/chartb, /params|, /charts|, /control| и т.д.) как в текущем App — без изменений. Сохранить семантику обратной связи: **layoutJson[i].sent = false** выставлять только в **updateWidget** и **apdateWidgetByArray** (при приходе ответа по topic); виджеты сами выставляют sent = true при отправке команды — не трогать.
+
+**Эмиты:**
+
+- После изменений layoutJson/pages: `eventEmitter.emit("layoutJsonUpdated", { layoutJson: this.layoutJson, pages: this.pages })`.
+- После изменений deviceList: `eventEmitter.emit("deviceListUpdated")`.
+
+**⏸ Проверка 1.** Остановиться. Попросить вас: открыть приложение в браузере; убедиться, что страница загружается (нет белого экрана и ошибок в консоли); при наличии устройства — что подключение к первому устройству происходит и запрос списка устройств уходит (в консоли/сети). После вашего подтверждения — продолжать.
+
+### 3. Рефакторинг App.svelte
+
+- **Создание менеджера:** один раз (вне onMount, на верхнем уровне скрипта) создать `const wsManager = new WebSocketManager(initialDeviceList, { debug })`. initialDeviceList — массив с одним элементом `{ name: "--", id: "--", ip: myip, ws: 0, status: false }` (myip из document.location.hostname / devMode).
+- **Локальный state только для реактивности:** оставить `let layoutJson = []; let pages = []; let deviceList = [];` (или один объект), подписаться в onMount на eventEmitter: `eventEmitter.on("layoutJsonUpdated", (data) => { layoutJson = data.layoutJson; pages = data.pages; })`, `eventEmitter.on("deviceListUpdated", () => { deviceList = wsManager.deviceList; })`. Так Svelte будет реагировать на обновления.
+- **Роутинг:** handleNavigation() устанавливает `wsManager.currentPageName = $router.path + "|"`, вызывает `wsManager.clearData()`, затем при "/|" — `wsManager.sendToAllDevices(wsManager.currentPageName)`, иначе — `wsManager.sendCurrentPageNameToSelectedWs()`. showDropdown выводить из currentPageName (например `$: showDropdown = currentPageName !== "/list|"`), currentPageName читать из wsManager.
+- **onMount:** await getUser() (api portal), записать результат в wsManager.userdata и wsManager.serverOnline; onCheck(); wsManager.firstDevListRequest = true; wsManager.selectedDeviceDataRefresh(); wsManager.connectToAllDevices(); wsManager.wsTestMsgTask() (или wsManager.startReconnectTask() если вынесли в менеджер); подписка на eventEmitter; вызов handleNavigation(). Не создавать больше локальные копии deviceList/layoutJson кроме тех, что обновляются по событиям.
+- **Шаблон:** везде, где сейчас передаются deviceList, layoutJson, pageReady, configJson, settingsJson и т.д., передавать либо из реактивных переменных (layoutJson, pages, deviceList), либо напрямую wsManager.* (pageReady, configJson, settingsJson, selectedWs, selectedDeviceData, и т.д.). Dropdown: bind:value={wsManager.selectedWs}, on:change={devicesDropdownChange}. devicesDropdownChange вызывает wsManager.selectedDeviceDataRefresh(), wsManager.clearData(), handleNavigation().
+- **Сохранение/команды:** логику формирования команды и отправки (saveConfig → /tuoyal|, /gifnoc|, /oiranecs|; saveSett, saveList, cleanLogs, wsPush → /control| и т.д.) перенести в методы менеджера (wsManager.saveConfig(), wsManager.saveSett(), …). В App остаются только тонкие обёртки, которые вызывают эти методы и при необходимости выставляют showAwaitingCircle / socketConnected (например rebootEsp, updateBuild). Так контракт страниц не меняется (они по-прежнему вызывают saveConfig(), rebootEsp() и т.д.), а реализация живёт в менеджере.
+- **getVersionsList, getModInfo, getProfile:** onParced переезжает в менеджер и там выставляет pageReady. Вызов HTTP (getVersionsList, getModInfo, getProfile) не переносить в менеджер. Вариант: при создании менеджера передать опции вида `{ onSystemParsed: async () => { const r = await getVersionsList(); ... wsManager.versionsList = r; }, onProfileParsed: async () => { await getModInfo(); await getProfile(); ... } }`. В onParced в менеджере для страницы system вызывать this.options.onSystemParsed?.(), для profile — this.options.onProfileParsed?.(). App в onMount задаёт эти колбэки (внутри них вызов api/portal и api/firmware и запись результата в wsManager).
+
+**⏸ Проверка 2.** Остановиться. Попросить вас: открыть приложение и проверить, что **все страницы работают** — открываются и при необходимости подгружают данные: **Управление** (dashboard, данные с устройств), **Конфигуратор**, **Подключение**, **Системные**, **Устройства**, **Вход**; при авторизации — **Модули (Profile)**. Ни одна страница не должна зависать на «Загрузка...» или показывать пустой/сломанный контент. После вашего подтверждения — продолжать.
+
+### 4. Совместимость и проверки
+
+- Не менять протокол (команды /gifnoc|, /tsil|, /control|, /params|, /charts|, форматы blob charta/chartb, цепочка firstDevListRequest → /devlist| → devlis → initDevList → connectToAllDevices).
+- **Чек-лист сохранности логики перед завершением (все страницы должны работать):**
+ - Список устройств: при первой загрузке отправляется `/devlist|` только с ws=0 при firstDevListRequest; по приходу devlis вызывается initDevList → devListOverride или devListCombine → снова connectToAllDevices.
+ - Управление (Dashboard): при `/|` рассылка всем; parseAllBlob при layout → combineLayoutsInOne → sortingLayout → `/params|`; при params → updateAllStatuses → `/charts|`; charta/chartb → apdateWidgetByArray по topic. **Обновление виджетов:** при приходе status/params/charta/chartb значения в виджетах (input, toggle, range, chart и т.д.) должны обновляться; после каждого такого обновления в менеджере — emit layoutJsonUpdated.
+ - Конфигуратор, Подключение, Системные, Устройства, Вход, Модули (Profile): те же команды и условия pageReady/parsed, что в текущем App.svelte; каждая страница открывается и получает свои данные.
+- **⏸ Проверка 3 (финальная).** Остановиться. Попросить вас: убедиться, что **все страницы работают** — Управление (в т.ч. графики), Конфигуратор, Подключение, Системные, Устройства, Вход, Модули (Profile при авторизации); загрузка приложения, смена маршрутов, dropdown, данные с устройств везде прилетают. По чек-листу из [SMOKE_TEST.md](SMOKE_TEST.md). После вашего подтверждения — считать рефакторинг завершённым.
+
+### 5. Итоговая структура
+
+- **eventEmitter.js** — глобальный шинный слой для уведомлений UI.
+- **WebSocketManager.js** — класс: состояние + делегирование в api/deviceSocket, lib/deviceConnection, lib/deviceListManager, lib/blobProtocol, lib/wsReconnect; эмиты при изменении layoutJson и deviceList.
+- **App.svelte** — создание wsManager, подписка на eventEmitter, синхронизация в локальные переменные для реактивности, handleNavigation, onMount (getUser, connectToAllDevices, wsTestMsgTask, подписки), тонкие обёртки (rebootEsp, saveConfig, ...), разметка и передача props в страницы.
+- **api/** и **lib/** — без изменений контрактов; вызываются только из WebSocketManager (и из App только getUser, getVersionsList и т.п.).
+
+Ожидаемый результат: App.svelte сократится до порядка 600–750 строк; одна точка входа для логики устройств (WebSocketManager); связь с UI через события.
\ No newline at end of file
diff --git a/SMOKE_TEST.md b/SMOKE_TEST.md
new file mode 100644
index 0000000..163261f
--- /dev/null
+++ b/SMOKE_TEST.md
@@ -0,0 +1,16 @@
+# Smoke test checklist (cleanup-and-verify)
+
+Run `npm run dev`, open http://localhost:8080 (or configured port).
+
+- [ ] App loads (no white screen); hash router works.
+- [ ] Dashboard (`/#/`): layout loads when device(s) connected; no NaN in charts.
+- [ ] List (`/#/list`): device list, add device, save list.
+- [ ] Config (`/#/config`): load configs from portal; export/import file (mark iotm, scenario=>).
+- [ ] Connection (`/#/connection`): SSID, settings, MQTT.
+- [ ] System (`/#/system`): versions list (ver.json), reboot.
+- [ ] Profile (`/#/profile`): user profile, compiler, install build (link uses portal.BASE).
+- [ ] Login (`/#/login`): login flow (CORS may block from localhost; deploy or proxy to verify).
+- [ ] Dropdown: switch device; selected device data updates.
+- [ ] Portal links: Config "publish" opens portal configs with token; Profile "install" uses portal build URL.
+
+Build: `npm run build` — must complete without errors.
diff --git a/src/App.svelte b/src/App.svelte
index c3c077c..9b3cec8 100644
--- a/src/App.svelte
+++ b/src/App.svelte
@@ -6,22 +6,12 @@
+43 67761588253
*/
- //6+49 кб 09/06/2023
- //6+51 кб 02/09/2023
- //6+64 кб 02/10/2023 + axios
- //6+53 кб 03/10/2023 + fetch
-
- //******************************************************import section*********************************************************/
- //*****************************************************************************************************************************/
import { onMount } from "svelte";
- import { Route, router, active } from "tinro";
+ import { Route, router } from "tinro";
router.mode.hash();
import Alarm from "./components/Alarm.svelte";
import Progress from "./components/Progress.svelte";
- //import Card from "./components/Card.svelte";
-
- //import ModalPass from "./components/ModalPass.svelte";
import DashboardPage from "./pages/Dashboard.svelte";
import ConfigPage from "./pages/Config.svelte";
import ConnectionPage from "./pages/Connection.svelte";
@@ -29,59 +19,28 @@
import SystemPage from "./pages/System.svelte";
import Login from "./pages/Login.svelte";
import Profile from "./pages/Profile.svelte";
- import { t, locale, locales } from "./i18n";
import Cookies from "js-cookie";
- //import UtilitiesPage from "./pages/Utilities.svelte";
- //import LogPage from "./pages/Log.svelte";
- //import AboutPage from "./pages/About.svelte";
-
import * as portal from "./api/portal.js";
import * as firmware from "./api/firmware.js";
- import * as deviceSocket from "./api/deviceSocket.js";
- import * as deviceConnection from "./lib/deviceConnection.js";
- import * as deviceListManager from "./lib/deviceListManager.js";
- import * as blobProtocol from "./lib/blobProtocol.js";
- import * as wsReconnect from "./lib/wsReconnect.js";
+ import WebSocketManager from "./lib/WebSocketManager.js";
+ import { eventEmitter } from "./eventEmitter.js";
import AppHeader from "./components/layout/AppHeader.svelte";
import AppNav from "./components/layout/AppNav.svelte";
import AppFooter from "./components/layout/AppFooter.svelte";
- //****************************************************constants section*********************************************************/
- //******************************************************************************************************************************/
- const debug = true;
- const LOG_MAX_MESSAGES = 100;
- let reconnectTimeout = 60; //период проверки соединения с устройством
- let remainingTimeout = reconnectTimeout;
- let preventReconnect = false;
- const waitingAckTimeout = 18000; //время ожидания ответа от устройства
- let rebootOrUpdateProcess = false;
- let rebootTimer;
- let opened = true;
- let preventMove = false;
- let screenSize;
- const blobDebug = false;
const devMode = true;
+ const myip = devMode ? "127.0.0.1" : document.location.hostname;
+ const initialDeviceList = [
+ { name: "--", id: "--", ip: myip, ws: 0, status: false },
+ ];
- let percent;
+ const wsManager = new WebSocketManager(initialDeviceList, { debug: true });
- //****************************************************variable section**********************************************************/
- //******************************************************************************************************************************/
- let myip = document.location.hostname;
- if (devMode) myip = "127.0.0.1";
-
- //Flags
- let firstDevListRequest = true;
- let showInput = false;
- let authorization = false;
- let showDropdown = true;
-
- let showAwaitingCircle = false;
-
- //dashboard
+ // Local reactive state (synced from wsManager via events)
+ let layoutJson = [];
let pages = [];
-
- //ready
+ let deviceList = [...initialDeviceList];
let pageReady = {
dash: false,
config: false,
@@ -89,897 +48,180 @@
list: false,
system: false,
dev: false,
+ profile: false,
};
-
- //update esp
- let versionsList = {};
- let choosingVersion = undefined;
-
- //JSON Files====================================
- let itemsJson = [];
- let widgetsJson = [];
let configJson = [];
let scenarioTxt = " ";
+ let widgetsJson = [];
+ let itemsJson = [];
let settingsJson = {};
- let ssidJson = {};
let errorsJson = {};
- let flashProfileJson = {};
- let otaJson = {};
- let deviceList = [];
- deviceList = [
- {
- name: "--",
- id: "--",
- ip: myip,
- ws: 0,
- status: false,
- },
- ];
-
- // ack state lives in wsReconnect.createAck
-
- let incDeviceList = [];
- let layoutJson = [];
- let paramsJson = {};
-
- let userdata = null;
- let allmodeinfo = null;
- let profile = null;
-
- let serverOnline = false;
-
- let parsed = {
- itemsJson: false,
- widgetsJson: false,
- configJson: false,
- scenarioTxt: false,
- settingsJson: false,
- ssidJson: false,
- incDeviceList: false,
- deviceListJson: false,
- errorsJson: false,
- statusJson: false,
- paramsJson: false,
- flashProfileJson: false,
- otaJson: false,
- };
-
- //===============================================
-
- // web sockets: pool in api/deviceSocket.js
- let socketConnected = false;
- let selectedDeviceData = undefined;
+ let ssidJson = {};
+ let versionsList = {};
+ let choosingVersion = undefined;
let selectedWs = 0;
- let originalWs = 0;
- let newDevice = {};
- let coreMessages = [];
+ let opened = true;
+ let preventMove = false;
+ let screenSize;
+ let showInput = false;
- //***********************************************************navigation********************************************************/
- let currentPageName = undefined;
+ $: currentPageName = wsManager.currentPageName;
+ $: remainingTimeout = wsManager.remainingTimeout;
+ $: wsManager.choosingVersion = choosingVersion;
router.subscribe(handleNavigation);
function handleNavigation() {
- currentPageName = $router.path.toString();
- currentPageName = currentPageName + "|";
-
- console.log("[i]", "user on page:", currentPageName);
-
- clearData();
-
- //если мы на странице dashboard то рассылаем всем устройствам запрос данных
- if (currentPageName === "/|") {
- sendToAllDevices(currentPageName);
- showDropdown = false;
- //в остальных случаях шлем только выбранному устройству запрос данных
+ wsManager.currentPageName = $router.path.toString() + "|";
+ console.log("[i]", "user on page:", wsManager.currentPageName);
+ wsManager.clearData();
+ if (wsManager.currentPageName === "/|") {
+ wsManager.sendToAllDevices(wsManager.currentPageName);
} else {
- if (currentPageName === "/list|") {
- //если мы перешли на страницу списка устройств отключаем выпадающий список
- showDropdown = false;
- } else {
- showDropdown = true;
- }
- //если мы на любой другой странице то запрашиваем данные
- sendCurrentPageNameToSelectedWs();
+ wsManager.sendCurrentPageNameToSelectedWs();
}
}
- function sendCurrentPageNameToSelectedWs() {
- if (selectedWs !== undefined) {
- wsSendMsg(selectedWs, currentPageName);
- }
- }
-
- //*******************************************************initialisation********************************************************************/
- onMount(async () => {
- console.log("[i]", "mounted");
- await getUser();
- onCheck();
- opened = screenSize > 900 ? true : false;
- selectedDeviceDataRefresh();
- //флаг первого запроса списка устройств
- firstDevListRequest = true;
- //вначале подключимся к известному нам ip этого устройства
- connectToAllDevices();
- wsTestMsgTask();
- //sortingLayout();
- });
-
- const getUser = async () => {
- const JWT = Cookies.get("token_iotm2");
- const res = await portal.getUser(JWT);
- if (res.ok) {
- userdata = res.userdata;
- serverOnline = true;
- } else {
- if (!res.serverOnline) serverOnline = false;
- else {
- console.log("error", "getUser");
- serverOnline = true;
- }
- }
- };
-
- //****************************************************web sockets section******************************************************/
- function getIP(ws) {
- return deviceConnection.getIP(ws, deviceList);
- }
-
- function wsSendMsg(ws, msg) {
- if (deviceSocket.send(ws, msg)) {
- if (debug) console.log("[i]", getIP(ws), ws, "msg send success", msg);
- } else {
- if (debug) console.log("[e]", getIP(ws), ws, "msg not send");
- }
- }
-
- const openHandler = () =>
- deviceConnection.createOpenHandler({
- markDeviceStatus,
- sendMsg: wsSendMsg,
- firstDevListRequest,
- currentPageName,
- selectedWs,
- sendCurrentPageNameToSelectedWs,
- });
-
- function messageHandler(ws, data) {
- if (typeof data === "string") {
- if (data === "/tstr|") ack(ws, true);
- return;
- }
- if (data instanceof Blob) {
- if (ws === selectedWs) parseBlob(data, ws);
- if (currentPageName === "/|") parseAllBlob(data, ws);
- }
- }
-
- function createConnection(wsIndex, ip) {
- if (ip === "error") {
- if (debug) console.log("[e]", "device list wrong");
- return;
- }
- if (debug) console.log("[i]", ip, wsIndex, "started connecting...");
- deviceSocket.createConnection(wsIndex, ip, {
- onOpen: (ws) => openHandler()(ws),
- onMessage: messageHandler,
- onClose: (ws) => markDeviceStatus(ws, false),
- onError: (ws) => markDeviceStatus(ws, false),
- });
- }
-
- function connectToAllDevices() {
- deviceConnection.connectToAllDevices(deviceList, getSelectedDeviceData, selectedWs, createConnection);
- }
-
- function printAllCreatedWs() {
- if (debug) console.log("[i]", "[ws]", "device count:", deviceList.length);
- }
-
- function markDeviceStatus(ws, status) {
- deviceList.forEach((device) => {
- if (device.ws === ws) {
- device.status = status;
- device.ping = 0;
- if (device.status === true) {
- console.log("[i]", device.ip, ws, "status online");
- } else {
- console.log("[i]", device.ip, ws, "status offline");
- deleteWidget(ws);
- sortingLayout(ws);
- }
- }
- });
- selectedDeviceDataRefresh();
- deviceList = deviceList;
- }
-
- function deleteWidget(ws) {
- layoutJson = layoutJson.filter((item) => item.ws !== ws);
- }
-
- const blobHandlers = {
- setItemsJson: (v) => (itemsJson = v),
- setParsedItemsJson: (v) => (parsed.itemsJson = v),
- setWidgetsJson: (v) => (widgetsJson = v),
- setParsedWidgetsJson: (v) => (parsed.widgetsJson = v),
- setConfigJson: (v) => (configJson = v),
- setParsedConfigJson: (v) => (parsed.configJson = v),
- setScenarioTxt: (v) => (scenarioTxt = v),
- setSettingsJson: (v) => (settingsJson = v),
- setParsedSettingsJson: (v) => (parsed.settingsJson = v),
- setSsidJson: (v) => (ssidJson = v),
- setParsedSsidJson: (v) => (parsed.ssidJson = v),
- setErrorsJson: (v) => (errorsJson = v),
- setParsedErrorsJson: (v) => (parsed.errorsJson = v),
- setParsedIncDeviceList: (v) => (parsed.incDeviceList = v),
- onDevlis: async (json) => {
- incDeviceList = json;
- deviceConnection.handleDevListReceived(incDeviceList, deviceList, firstDevListRequest, {
- setDeviceList: (list) => (deviceList = list),
- setFirstDevListRequest: (v) => (firstDevListRequest = v),
- setParsedDeviceListJson: (v) => (parsed.deviceListJson = v),
- onParced,
- selectedDeviceDataRefresh,
- connectToAllDevices,
- });
- },
- setFlashProfileJson: (v) => (flashProfileJson = v),
- setParsedFlashProfileJson: (v) => (parsed.flashProfileJson = v),
- setOtaJson: (v) => (otaJson = v),
- setParsedOtaJson: (v) => (parsed.otaJson = v),
- addCoreMsg: (msg) => addCoreMsg(msg),
- onParced: () => onParced(),
- };
-
- async function parseBlob(blob, ws) {
- await blobProtocol.parseBlob(blob, ws, blobHandlers);
- }
-
- const allBlobHandlers = {
- updateWidget: (v) => updateWidget(v),
- combineLayoutsInOne: (ws, layout) => combineLayoutsInOne(ws, layout),
- mergeParams: (devParams) => {
- paramsJson = { ...paramsJson, ...devParams };
- paramsJson = paramsJson;
- },
- updateAllStatuses: (ws) => updateAllStatuses(ws),
- onParced: () => onParced(),
- apdateWidgetByArray: (v) => apdateWidgetByArray(v),
- };
-
- async function parseAllBlob(blob, ws) {
- await blobProtocol.parseAllBlob(blob, ws, allBlobHandlers);
- }
-
- async function onParced() {
- if (currentPageName === "/|") {
- pageReady.dash = true;
- }
-
- if (currentPageName === "/config|" && parsed.itemsJson && parsed.widgetsJson && parsed.configJson && parsed.settingsJson) {
- clearParcedFlags();
- pageReady.config = true;
- if (debug) console.log("✔✔", "config page parced");
- }
-
- //&& parsed.widgetsJson && parsed.configJson - добавить когда 451 прошивка уйдет в прошлое
- if (currentPageName === "/connection|" && parsed.ssidJson && parsed.settingsJson && parsed.errorsJson) {
- clearParcedFlags();
- if (debug) console.log("✔✔", "connection page parced");
- pageReady.connection = true;
- }
-
- if (currentPageName === "/list|" && parsed.settingsJson) {
- clearParcedFlags();
- if (debug) console.log("✔✔", "list page parced");
- pageReady.list = true;
- }
-
- if (currentPageName === "/system|" && parsed.errorsJson && parsed.settingsJson) {
- clearParcedFlags();
- getVersionsList();
- if (debug) console.log("✔✔", "system page parced");
- pageReady.system = true;
- }
-
- //&& parsed.otaJson
- if (currentPageName === "/profile|" && parsed.flashProfileJson) {
- clearParcedFlags();
- if (debug) console.log("✔✔", "profile page parced");
- pageReady.profile = true;
- await getModInfo();
- await getProfile();
- }
- }
-
- const getModInfo = async () => {
- const res = await portal.getModInfo();
- if (res.ok) allmodeinfo = res.allmodeinfo;
- else console.log("error", "getModInfo");
- };
-
- const getProfile = async () => {
- const JWT = Cookies.get("token_iotm2");
- const res = await portal.getProfile(JWT);
- if (res.ok) {
- profile = res.profile;
- await markProfileAsPerThisDevProfile();
- } else console.log("error", "getProfile");
- };
-
- const markProfileAsPerThisDevProfile = async () => {
- profile.projectProp.platformio.default_envs = flashProfileJson.projectProp.platformio.default_envs;
- for (const [compilerCategory, compilerCategoryModules] of Object.entries(profile.modules)) {
- let devCategoryModules = flashProfileJson.modules[compilerCategory];
- compilerCategoryModules.forEach((compilerModule) => {
- compilerModule.active = false;
- if (devCategoryModules) {
- devCategoryModules.forEach((devModule) => {
- if (devModule.path === compilerModule.path) {
- compilerModule.active = devModule.active;
- }
- });
- }
- });
- }
- };
-
- function devListOverride() {
- deviceList = deviceListManager.devListOverride(incDeviceList);
- console.log("[i]", "[devlist]", "devlist overrided");
- }
-
- function devListCombine() {
- deviceList = deviceListManager.devListCombine(deviceList, incDeviceList);
- console.log("[i]", "[devlist]", "devlist combined");
- }
-
- function combineArrays(A, B) {
- return deviceListManager.combineArrays(A, B);
- }
-
- //***********************************************************dashboard***************************************************************/
-
- //слияние layout-ов всех устройств в общий layout
- async function combineLayoutsInOne(ws, devLayout) {
- for (let i = 0; i < devLayout.length; i++) {
- devLayout[i].ws = ws;
- }
- layoutJson = layoutJson.concat(devLayout);
- console.log("[2]", ws, "devLayout pushed to layout");
- sortingLayout(ws);
- }
-
- function sortingLayout(ws) {
- //сортируем весь layout по алфавиту
- layoutJson.sort(function (a, b) {
- if (a.descr < b.descr) {
- return -1;
- }
- if (a.descr > b.descr) {
- return 1;
- }
- return 0;
- });
- //формируем json всех карточек
- pages = [];
- const newPage = Array.from(new Set(Array.from(layoutJson, ({ page }) => page)));
- newPage.forEach(function (item, i, arr) {
- pages = [
- ...pages,
- JSON.parse(
- JSON.stringify({
- page: item,
- })
- ),
- ];
- });
- //сортируем карточки по алфавиту
- pages.sort(function (a, b) {
- if (a.page < b.page) {
- return -1;
- }
- if (a.page > b.page) {
- return 1;
- }
- return 0;
- });
-
- layoutJson = layoutJson;
- console.log("[3]", ws, "layout sort, requested params...");
- wsSendMsg(ws, "/params|");
- }
-
- function updateAllStatuses(ws) {
- for (const [key, value] of Object.entries(paramsJson)) {
- for (let i = 0; i < layoutJson.length; i++) {
- let topic = layoutJson[i].topic;
- if (topic) {
- //layoutJson[i].ws = ws;
- topic = topic.substring(topic.lastIndexOf("/") + 1, topic.length);
- if (key === topic) {
- console.log("[i]", "updated =>" + topic, value);
- layoutJson[i].status = value;
- break;
- }
- }
- }
- }
- wsSendMsg(ws, "/charts|");
- }
-
- //обработка интервально прилетающих статусов
- function updateWidget(newStatusJson) {
- for (let i = 0; i < layoutJson.length; i++) {
- let topic = layoutJson[i].topic;
- if (topic === newStatusJson.topic) {
- layoutJson[i] = jsonConcat(layoutJson[i], newStatusJson);
- //получен ответ - выключаем красный цвет
- layoutJson[i].sent = false;
- break;
- }
- }
- }
-
- //если статус виджета это массив и его нужно накопить
- //должна вызываться когда весь layout в сборе
- async function apdateWidgetByArray(newStatusJson) {
- console.log("[i]", "collecting arrays");
- let error = true;
- if (layoutJson.length > 0) {
- for (let i = 0; i < layoutJson.length; i++) {
- let topic = layoutJson[i].topic;
- if (topic === newStatusJson.topic) {
- error = false;
- layoutJson[i] = jsonConcatEx(layoutJson[i], newStatusJson);
- let prevArr = layoutJson[i].status; //который был в layout
- let newArr = newStatusJson.status; //тот что получили
- if (prevArr) {
- //если что то было в layout то делаем слияние
- prevArr = [...prevArr, ...newArr];
- layoutJson[i].status = prevArr;
- } else {
- //если ничего не было то просто запишем новый
- layoutJson[i].status = newArr;
- }
- //получен ответ - выключаем красный цвет
- layoutJson[i].sent = false;
- }
- }
- } else {
- console.log("[E]", "layoutJson missing");
- }
- if (error) console.log("[E]", "topic not found ", newStatusJson.topic);
- }
-
- function jsonConcat(o1, o2) {
- for (var key in o2) {
- o1[key] = o2[key];
- }
- return o1;
- }
-
- //объединяем исклчая статус так как статус в данном случае накопительная переменная
- function jsonConcatEx(o1, o2) {
- for (var key in o2) {
- if (key !== "status") {
- o1[key] = o2[key];
- }
- }
- return o1;
- }
-
- function saveConfig() {
- wsSendMsg(selectedWs, "/tuoyal|" + JSON.stringify(generateLayout()));
- modify();
- wsSendMsg(selectedWs, "/gifnoc|" + JSON.stringify(configJson));
-
- wsSendMsg(selectedWs, "/oiranecs|" + scenarioTxt);
- clearData();
- sendCurrentPageNameToSelectedWs();
- }
-
- function saveSett() {
- var size = Object.keys(settingsJson).length;
- console.log("[i]", "settingsJson length: " + size);
- if (size > 5) {
- jsonArrWrite(deviceList, "ip", getIP(selectedWs), "name", settingsJson.name);
- deviceList = deviceList;
- wsSendMsg(selectedWs, "/sgnittes|" + JSON.stringify(settingsJson));
- } else {
- window.alert("Ошибка размера settingsJson (возможно не был передан странице)");
- }
- clearData();
- sendCurrentPageNameToSelectedWs();
- }
-
- function saveList() {
- //при сохранении списка в память необходимо удалить все статусы
- let devListForSave = Object.assign([], deviceList);
- for (let i = 0; i < devListForSave.length; i++) {
- //delete devListForSave[i].status;
- devListForSave[i].status = false;
- }
- wsSendMsg(selectedWs, "/tsil|" + JSON.stringify(devListForSave));
- }
-
- function cleanLogs() {
- wsSendMsg(selectedWs, "/clean|");
- }
-
- function saveMqtt() {
- var size = Object.keys(settingsJson).length;
- wsSendMsg(selectedWs, "/tuoyal|" + JSON.stringify(generateLayout()));
- if (size > 5) {
- wsSendMsg(selectedWs, "/sgnittes|" + JSON.stringify(settingsJson));
- } else {
- window.alert("Ошибка");
- }
- clearData();
- wsSendMsg(selectedWs, "/mqtt|");
- }
-
- function getInput() {
- let input = {
- name: "inputDate",
- //descr: "Выберите дату",
- widget: "input",
- size: "small",
- color: "orange",
- type: "date",
- };
- return input;
- }
-
- function modify() {
- for (let i = 0; i < configJson.length; i++) {
- let config = configJson[i];
- delete config["show"];
- }
- }
-
- //по конфигу делаем виджеты
- function generateLayout() {
- let layout = [];
- for (let i = 0; i < configJson.length; i++) {
- let config = Object.assign({}, configJson[i]);
- let setWidget = config.widget;
- let error = true;
- for (let w = 0; w < widgetsJson.length; w++) {
- if (setWidget === widgetsJson[w].name) {
- let widget = Object.assign({}, widgetsJson[w]);
- widget.page = config.page;
- widget.descr = config.descr;
- widget.topic = settingsJson.mqttPrefix + "/" + settingsJson.id + "/" + config.id;
- if (setWidget !== "nil") layout.push(widget);
- //создаем графики с окнами ввода
- if (widget.widget === "chart" && widget.type !== "bar") {
- let input = getInput();
- input.page = config.page;
- input.topic = settingsJson.mqttPrefix + "/" + settingsJson.id + "/" + config.id + "-date";
- input.descr = config.descr;
- //console.log("[i]", "topic ", widget.topic);
- layout.push(input);
- }
- error = false;
- break;
- } else {
- error = true;
- }
- }
- if (error) console.log("[E]", "error, widget not found: " + setWidget);
- }
-
- //сортируем весь layout по алфавиту
- layout.sort(function (a, b) {
- if (a.descr < b.descr) {
- return -1;
- }
- if (a.descr > b.descr) {
- return 1;
- }
- return 0;
- });
-
- for (let i = 0; i < layout.length; i++) {
- layout[i].order = i;
- }
-
- return layout;
- }
-
- function clearData() {
- itemsJson = [];
- widgetsJson = [];
- configJson = [];
- scenarioTxt = " ";
- settingsJson = {};
- //ssidJson = {};
- errorsJson = {};
- layoutJson = [];
- paramsJson = {};
- otaJson = {};
- flashProfileJson = {};
-
- //incDeviceList = [];
-
- for (const [key, value] of Object.entries(pageReady)) {
- pageReady[key] = false;
- }
-
- clearParcedFlags();
-
- if (debug) console.log("[i]", "all json files cleared");
- }
-
- function clearParcedFlags() {
- console.log("[i]", "parced flags cleared");
- for (const [key, value] of Object.entries(parsed)) {
- parsed[key] = false;
- }
- }
-
- function wsPush(ws, topic, status) {
- let msg = topic + " " + status;
- if (debug) console.log("[i]", "ws: ", ws, msg);
- //layoutJson = layoutJson;
- let key = topic.substring(topic.lastIndexOf("/") + 1, topic.length);
- wsSendMsg(ws, "/control|" + key + "/" + status);
- }
-
- const ack = wsReconnect.createAck({
- markDeviceStatus,
- getDeviceList: () => deviceList,
- setDeviceList: (list) => (deviceList = list),
- waitingAckTimeout,
- });
-
- const wsTestMsgTask = wsReconnect.createWsTestMsgTask({
- getDeviceList: () => deviceList,
- send: wsSendMsg,
- markDeviceStatus,
- connectDevice: (ws) => createConnection(ws, getIP(ws)),
- ack,
- getRemainingTimeout: () => remainingTimeout,
- setRemainingTimeout: (v) => (remainingTimeout = v),
- reconnectTimeout,
- getPreventReconnect: () => preventReconnect,
- setPercent: (v) => (percent = v),
- getRebootOrUpdateProcess: () => rebootOrUpdateProcess,
- setRebootOrUpdateProcess: (v) => (rebootOrUpdateProcess = v),
- getSocketConnected: () => socketConnected,
- setShowAwaitingCircle: (v) => (showAwaitingCircle = v),
- setReconnectTimeout: (v) => (reconnectTimeout = v),
- printAllCreatedWs,
- });
-
- function sendToAllDevices(msg) {
- deviceList.forEach((device) => {
- if (device.status === true) {
- wsSendMsg(device.ws, msg);
- }
- });
- }
-
- //***********************************************************logging******************************************************************/
- const addCoreMsg = (msg) => {
- if (coreMessages.length >= LOG_MAX_MESSAGES) {
- coreMessages.shift();
- }
- //const time = new Date().getTime();
- coreMessages = [
- ...coreMessages,
- {
- msg,
- },
- ];
- coreMessages.sort(function (a, b) {
- if (a.time > b.time) {
- return -1;
- }
- if (a.time < b.time) {
- return 1;
- }
- return 0;
- });
- };
-
- //***********************************************************dev list******************************************************************/
-
- //всякий раз когда список устройств был обновлен
- function selectedDeviceDataRefresh() {
- //запишем в переменную selectedDeviceData выбранное устройство, что бы в коде было известно выбранное устройство
- getSelectedDeviceData(selectedWs);
- socketConnected = selectedDeviceData.status;
- }
-
function devicesDropdownChange() {
+ wsManager.selectedWs = selectedWs;
if (currentPageName === "/list|") {
console.log("[i]", "user change dropdown on list page!!!");
} else {
- selectedDeviceDataRefresh();
- clearData();
- //запускаем навигацию что дать контроллеру запрос данных
+ wsManager.selectedDeviceDataRefresh();
+ wsManager.clearData();
handleNavigation();
- if (debug) console.log("[i]", "user selected device:", selectedDeviceData.name);
- if (selectedDeviceData.ip === myip) {
- originalWs = selectedWs;
- if (debug) console.log("[i]", "user selected original device", selectedDeviceData.name);
- }
+ console.log("[i]", "user selected device:", wsManager.selectedDeviceData?.name);
}
}
- //функция которая записывает в переменную данные выбранного юзером устройства
- function getSelectedDeviceData(ws) {
- for (let i = 0; i < deviceList.length; i++) {
- let device = deviceList[i];
- if (device.ws === ws) {
- selectedDeviceData = device;
- break;
- }
- }
+ function onCheck() {
+ preventMove = screenSize < 900;
}
- function addDevInList() {
- if (!showInput) {
- if (newDevice.name !== undefined && newDevice.ip !== undefined && newDevice.id !== undefined) {
- newDevice.status = false;
- newDevice.ws = deviceList.length;
- incDeviceList.push(newDevice);
- devListCombine();
- //onParced();
- //selectedDeviceDataRefresh();
- connectToAllDevices();
- if (debug) console.log("[i]", "selected device: ", selectedDeviceData);
- return true;
+ onMount(async () => {
+ console.log("[i]", "mounted");
+ const JWT = Cookies.get("token_iotm2");
+ const res = await portal.getUser(JWT);
+ if (res.ok) {
+ wsManager.userdata = res.userdata;
+ wsManager.serverOnline = true;
+ } else {
+ wsManager.serverOnline = res.serverOnline !== false;
+ }
+
+ wsManager.options.onSystemParsed = async () => {
+ const r = await firmware.getVersionsList(wsManager.settingsJson.serverip);
+ if (r.ok && r.data) {
+ wsManager.versionsList = r.data[wsManager.errorsJson.bn] || {};
+ wsManager.choosingVersion = wsManager.errorsJson.bver;
} else {
- if (debug) console.log("[e]", "wrong data");
- return false;
+ wsManager.choosingVersion = undefined;
}
- }
- }
+ };
- function jsonArrWrite(jsonArr, idKey, idValue, paramKey, paramValue) {
- for (let i = 0; i < jsonArr.length; i++) {
- let obj = jsonArr[i];
- for (const [key, value] of Object.entries(obj)) {
- if (key == idKey && value == idValue) {
- obj[paramKey] = paramValue;
- break;
+ wsManager.options.onProfileParsed = async () => {
+ const modRes = await portal.getModInfo();
+ if (modRes.ok) wsManager.allmodeinfo = modRes.allmodeinfo;
+ const JWT2 = Cookies.get("token_iotm2");
+ const profRes = await portal.getProfile(JWT2);
+ if (profRes.ok) {
+ wsManager.profile = profRes.profile;
+ const p = wsManager.profile;
+ const fp = wsManager.flashProfileJson;
+ if (p && fp) {
+ p.projectProp.platformio.default_envs = fp.projectProp?.platformio?.default_envs;
+ for (const [compilerCategory, compilerCategoryModules] of Object.entries(p.modules || {})) {
+ const devCategoryModules = fp.modules?.[compilerCategory];
+ compilerCategoryModules.forEach((compilerModule) => {
+ compilerModule.active = false;
+ (devCategoryModules || []).forEach((devModule) => {
+ if (devModule.path === compilerModule.path) compilerModule.active = devModule.active;
+ });
+ });
+ }
}
}
- }
- }
-
- //**********************************************************modal*************************************************************************/
- function onCheck() {
- if (screenSize < 900) {
- preventMove = true;
- } else {
- preventMove = false;
- }
- }
-
- //************************************************elements and presets dropdown************************************************************/
-
- function ssidClick() {
- wsSendMsg(selectedWs, "/scan|");
- }
-
- function rebootEsp() {
- rebootOrUpdateProcess = true;
- if (debug) console.log("[i]", "reboot...");
- wsSendMsg(selectedWs, "/reboot|");
- markDeviceStatus(selectedWs, false);
- showAwaitingCircle = true;
- socketConnected = false;
- reconnectTimeout = 10;
- remainingTimeout = reconnectTimeout;
- }
-
- function updateBuild(path) {
- rebootOrUpdateProcess = true;
- console.log(path);
- wsSendMsg(selectedWs, "/update|" + path);
- showAwaitingCircle = true;
- socketConnected = false;
- reconnectTimeout = 20;
- remainingTimeout = reconnectTimeout;
- }
-
- function applicationReboot() {
- console.log("[i]", "reboot svelte...");
- for (const [key, value] of Object.entries(pageReady)) {
- pageReady[key] = false;
- }
- showAwaitingCircle = true;
- setTimeout(() => {
- location.reload();
- }, 1000);
- }
-
- function cancelAlarm(alarmKey) {
- console.log("[x]", alarmKey);
- errorsJson[alarmKey] = 0;
- wsSendMsg(selectedWs, '/rorre|{"' + alarmKey + '":0}');
- }
-
- //************************************************update esp firm************************************************************//
-
- async function getVersionsList() {
- versionsList = {};
- const res = await firmware.getVersionsList(settingsJson.serverip);
- if (res.ok && res.data) {
- versionsList = res.data[errorsJson.bn];
- choosingVersion = errorsJson.bver;
- console.log(JSON.stringify(versionsList));
- } else {
- choosingVersion = undefined;
- if (settingsJson.serverip) console.log("error, versions list not received");
- }
- }
-
- function moduleOrder(id, key, value) {
- console.log("order: ", id, key, value);
- let json = {
- id: id,
- key: key,
- value: value,
};
- console.log(json);
- wsSendMsg(selectedWs, "/order|" + JSON.stringify(json));
- }
+
+ onCheck();
+ opened = screenSize > 900;
+ wsManager.firstDevListRequest = true;
+ wsManager.selectedDeviceDataRefresh();
+ wsManager.connectToAllDevices();
+ wsManager.startReconnectTask();
+
+ eventEmitter.on("layoutJsonUpdated", (data) => {
+ layoutJson = data.layoutJson || [];
+ pages = data.pages || [];
+ if (data.pageReady) pageReady = data.pageReady;
+ });
+ eventEmitter.on("deviceListUpdated", () => {
+ deviceList = wsManager.deviceList;
+ });
+ eventEmitter.on("configUpdated", (data) => {
+ configJson = data.configJson || [];
+ scenarioTxt = data.scenarioTxt ?? " ";
+ if (data.widgetsJson) widgetsJson = data.widgetsJson;
+ if (data.itemsJson) itemsJson = data.itemsJson;
+ });
+ eventEmitter.on("connectionUpdated", (data) => {
+ if (data.settingsJson) settingsJson = data.settingsJson;
+ if (data.errorsJson) errorsJson = data.errorsJson;
+ if (data.ssidJson) ssidJson = data.ssidJson;
+ });
+ eventEmitter.on("systemUpdated", () => {
+ versionsList = wsManager.versionsList || {};
+ choosingVersion = wsManager.choosingVersion;
+ });
+
+ handleNavigation();
+ });
- {#if showAwaitingCircle}
+ {#if wsManager.showAwaitingCircle}
{/if}
-
-
-
onCheck()} {userdata} />
+ onCheck()} userdata={wsManager.userdata} />
- {#if !socketConnected && currentPageName != "/|"}
+ {#if !wsManager.socketConnected && wsManager.currentPageName !== "/|"}
{:else}
- wsPush(ws, topic, status)} />
+ wsManager.wsPush(ws, topic, status)} />
- saveConfig()} cleanLogs={() => cleanLogs()} rebootEsp={() => rebootEsp()} moduleOrder={(id, key, value) => moduleOrder(id, key, value)} userdata={userdata} />
+ { wsManager.configJson = configJson; wsManager.scenarioTxt = scenarioTxt; wsManager.saveConfig(); }} cleanLogs={() => wsManager.cleanLogs()} rebootEsp={() => wsManager.rebootEsp()} moduleOrder={(id, key, value) => wsManager.moduleOrder(id, key, value)} userdata={wsManager.userdata} />
- rebootEsp()} ssidClick={() => ssidClick()} saveSett={() => saveSett()} saveMqtt={() => saveMqtt()} settingsJson={settingsJson} errorsJson={errorsJson} ssidJson={ssidJson} />
+ wsManager.rebootEsp()} ssidClick={() => wsManager.ssidClick()} saveSett={() => { wsManager.settingsJson = settingsJson; wsManager.saveSett(); }} saveMqtt={() => { wsManager.settingsJson = settingsJson; wsManager.saveMqtt(); }} {settingsJson} {errorsJson} {ssidJson} />
- saveSett()} rebootEsp={() => rebootEsp()} showInput={showInput} addDevInList={() => addDevInList()} newDevice={newDevice} sendToAllDevices={(msg) => sendToAllDevices(msg)} saveList={() => saveList()} percent={percent} devListOverride={() => devListOverride()} applicationReboot={() => applicationReboot()} />
+ wsManager.saveSett()} rebootEsp={() => wsManager.rebootEsp()} showInput={showInput} addDevInList={() => wsManager.addDevInList()} newDevice={wsManager.newDevice} sendToAllDevices={(msg) => wsManager.sendToAllDevices(msg)} saveList={() => wsManager.saveList()} percent={wsManager.percent} devListOverride={() => wsManager.devListOverride()} applicationReboot={() => wsManager.applicationReboot()} />
- saveSett()} rebootEsp={() => rebootEsp()} cleanLogs={() => cleanLogs()} cancelAlarm={(alarmKey) => cancelAlarm(alarmKey)} versionsList={versionsList} bind:choosingVersion={choosingVersion} coreMessages={coreMessages} />
+ wsManager.saveSett()} rebootEsp={() => wsManager.rebootEsp()} cleanLogs={() => wsManager.cleanLogs()} cancelAlarm={(alarmKey) => wsManager.cancelAlarm(alarmKey)} versionsList={versionsList} bind:choosingVersion coreMessages={wsManager.coreMessages} />
- updateBuild(path)} allmodeinfo={allmodeinfo} profile={profile} serverOnline={serverOnline} otaJson={otaJson} />
+ wsManager.updateBuild(path)} allmodeinfo={wsManager.allmodeinfo} profile={wsManager.profile} serverOnline={wsManager.serverOnline} otaJson={wsManager.otaJson} />
-
+
{/if}
diff --git a/src/api/portal.js b/src/api/portal.js
index 23f2291..d69f4db 100644
--- a/src/api/portal.js
+++ b/src/api/portal.js
@@ -9,7 +9,8 @@
import { get, post } from "./http.js";
-const BASE = "https://portal.iotmanager.org";
+/** Portal base URL; export for building links in UI (e.g. configs, build install). */
+export const BASE = "https://portal.iotmanager.org";
function authHeaders(token) {
return {
diff --git a/src/eventEmitter.js b/src/eventEmitter.js
new file mode 100644
index 0000000..e86f0e5
--- /dev/null
+++ b/src/eventEmitter.js
@@ -0,0 +1,32 @@
+/**
+ * Simple event bus for UI notifications (layoutJsonUpdated, deviceListUpdated).
+ * Used by WebSocketManager to notify App.svelte without tight coupling.
+ */
+class EventEmitter {
+ constructor() {
+ this._listeners = {};
+ }
+
+ on(event, listener) {
+ if (!this._listeners[event]) this._listeners[event] = [];
+ this._listeners[event].push(listener);
+ }
+
+ off(event, listener) {
+ if (!this._listeners[event]) return;
+ this._listeners[event] = this._listeners[event].filter((l) => l !== listener);
+ }
+
+ emit(event, ...args) {
+ if (!this._listeners[event]) return;
+ this._listeners[event].forEach((l) => {
+ try {
+ l(...args);
+ } catch (e) {
+ console.error("[eventEmitter]", event, e);
+ }
+ });
+ }
+}
+
+export const eventEmitter = new EventEmitter();
diff --git a/src/lib/WebSocketManager.js b/src/lib/WebSocketManager.js
new file mode 100644
index 0000000..9777adc
--- /dev/null
+++ b/src/lib/WebSocketManager.js
@@ -0,0 +1,705 @@
+/**
+ * WebSocketManager: owns device/WS state and delegates to api/ and lib/.
+ * Emits layoutJsonUpdated and deviceListUpdated for App.svelte reactivity.
+ */
+
+import * as deviceSocket from "../api/deviceSocket.js";
+import * as deviceConnection from "./deviceConnection.js";
+import * as deviceListManager from "./deviceListManager.js";
+import * as blobProtocol from "./blobProtocol.js";
+import * as wsReconnect from "./wsReconnect.js";
+import { eventEmitter } from "../eventEmitter.js";
+
+const LOG_MAX_MESSAGES = 100;
+const WAITING_ACK_TIMEOUT = 18000;
+const DEFAULT_RECONNECT_TIMEOUT = 60;
+const debug = true;
+
+export default class WebSocketManager {
+ constructor(initialDeviceList, options = {}) {
+ this.options = options;
+ this.debug = options.debug !== false && debug;
+
+ // State (mirror of App.svelte)
+ this.deviceList = Array.isArray(initialDeviceList) ? [...initialDeviceList] : [];
+ this.layoutJson = [];
+ this.paramsJson = {};
+ this.pages = [];
+ this.parsed = {
+ itemsJson: false,
+ widgetsJson: false,
+ configJson: false,
+ scenarioTxt: false,
+ settingsJson: false,
+ ssidJson: false,
+ incDeviceList: false,
+ deviceListJson: false,
+ errorsJson: false,
+ statusJson: false,
+ paramsJson: false,
+ flashProfileJson: false,
+ otaJson: false,
+ };
+ this.pageReady = {
+ dash: false,
+ config: false,
+ connection: false,
+ list: false,
+ system: false,
+ dev: false,
+ profile: false,
+ };
+ this.firstDevListRequest = true;
+ this.currentPageName = undefined;
+ this.selectedWs = 0;
+ this.selectedDeviceData = undefined;
+ this.socketConnected = false;
+ this.originalWs = 0;
+ this.itemsJson = [];
+ this.widgetsJson = [];
+ this.configJson = [];
+ this.scenarioTxt = " ";
+ this.settingsJson = {};
+ this.ssidJson = {};
+ this.errorsJson = {};
+ this.incDeviceList = [];
+ this.flashProfileJson = {};
+ this.otaJson = {};
+ this.newDevice = {};
+ this.coreMessages = [];
+ this.reconnectTimeout = DEFAULT_RECONNECT_TIMEOUT;
+ this.remainingTimeout = this.reconnectTimeout;
+ this.preventReconnect = false;
+ this.rebootOrUpdateProcess = false;
+ this.showAwaitingCircle = false;
+ this.percent = 0;
+ this.userdata = null;
+ this.serverOnline = false;
+ this.allmodeinfo = null;
+ this.profile = null;
+ this.versionsList = {};
+ this.choosingVersion = undefined;
+
+ // Build blob handlers that call this.*
+ this._blobHandlers = {
+ setItemsJson: (v) => (this.itemsJson = v),
+ setParsedItemsJson: (v) => (this.parsed.itemsJson = v),
+ setWidgetsJson: (v) => (this.widgetsJson = v),
+ setParsedWidgetsJson: (v) => (this.parsed.widgetsJson = v),
+ setConfigJson: (v) => (this.configJson = v),
+ setParsedConfigJson: (v) => (this.parsed.configJson = v),
+ setScenarioTxt: (v) => (this.scenarioTxt = v),
+ setSettingsJson: (v) => (this.settingsJson = v),
+ setParsedSettingsJson: (v) => (this.parsed.settingsJson = v),
+ setSsidJson: (v) => (this.ssidJson = v),
+ setParsedSsidJson: (v) => (this.parsed.ssidJson = v),
+ setErrorsJson: (v) => (this.errorsJson = v),
+ setParsedErrorsJson: (v) => (this.parsed.errorsJson = v),
+ setParsedIncDeviceList: (v) => (this.parsed.incDeviceList = v),
+ onDevlis: async (json) => {
+ this.incDeviceList = json;
+ deviceConnection.handleDevListReceived(this.incDeviceList, this.deviceList, this.firstDevListRequest, {
+ setDeviceList: (list) => {
+ this.deviceList = list;
+ eventEmitter.emit("deviceListUpdated");
+ },
+ setFirstDevListRequest: (v) => (this.firstDevListRequest = v),
+ setParsedDeviceListJson: (v) => (this.parsed.deviceListJson = v),
+ onParced: () => this.onParced(),
+ selectedDeviceDataRefresh: () => this.selectedDeviceDataRefresh(),
+ connectToAllDevices: () => this.connectToAllDevices(),
+ });
+ },
+ setFlashProfileJson: (v) => (this.flashProfileJson = v),
+ setParsedFlashProfileJson: (v) => (this.parsed.flashProfileJson = v),
+ setOtaJson: (v) => (this.otaJson = v),
+ setParsedOtaJson: (v) => (this.parsed.otaJson = v),
+ addCoreMsg: (msg) => this.addCoreMsg(msg),
+ onParced: () => this.onParced(),
+ };
+
+ this._allBlobHandlers = {
+ updateWidget: (v) => this.updateWidget(v),
+ combineLayoutsInOne: (ws, layout) => this.combineLayoutsInOne(ws, layout),
+ mergeParams: (devParams) => {
+ this.paramsJson = { ...this.paramsJson, ...devParams };
+ },
+ updateAllStatuses: (ws) => this.updateAllStatuses(ws),
+ onParced: () => this.onParced(),
+ apdateWidgetByArray: (v) => this.apdateWidgetByArray(v),
+ };
+
+ this.ack = wsReconnect.createAck({
+ markDeviceStatus: (ws, st) => this.markDeviceStatus(ws, st),
+ getDeviceList: () => this.deviceList,
+ setDeviceList: (list) => {
+ this.deviceList = list;
+ eventEmitter.emit("deviceListUpdated");
+ },
+ waitingAckTimeout: WAITING_ACK_TIMEOUT,
+ });
+
+ this.wsTestMsgTask = wsReconnect.createWsTestMsgTask({
+ getDeviceList: () => this.deviceList,
+ send: (ws, msg) => this.wsSendMsg(ws, msg),
+ markDeviceStatus: (ws, st) => this.markDeviceStatus(ws, st),
+ connectDevice: (ws) => this._createConnection(ws, this.getIP(ws)),
+ ack: (ws, st) => this.ack(ws, st),
+ getRemainingTimeout: () => this.remainingTimeout,
+ setRemainingTimeout: (v) => (this.remainingTimeout = v),
+ reconnectTimeout: this.reconnectTimeout,
+ getPreventReconnect: () => this.preventReconnect,
+ setPercent: (v) => (this.percent = v),
+ getRebootOrUpdateProcess: () => this.rebootOrUpdateProcess,
+ setRebootOrUpdateProcess: (v) => (this.rebootOrUpdateProcess = v),
+ getSocketConnected: () => this.socketConnected,
+ setShowAwaitingCircle: (v) => (this.showAwaitingCircle = v),
+ setReconnectTimeout: (v) => (this.reconnectTimeout = v),
+ printAllCreatedWs: () => this._printAllCreatedWs(),
+ });
+ }
+
+ getIP(ws) {
+ return deviceConnection.getIP(ws, this.deviceList);
+ }
+
+ wsSendMsg(ws, msg) {
+ if (deviceSocket.send(ws, msg)) {
+ if (this.debug) console.log("[i]", this.getIP(ws), ws, "msg send success", msg);
+ } else {
+ if (this.debug) console.log("[e]", this.getIP(ws), ws, "msg not send");
+ }
+ }
+
+ _createConnection(wsIndex, ip) {
+ if (ip === "error") {
+ if (this.debug) console.log("[e]", "device list wrong");
+ return;
+ }
+ if (this.debug) console.log("[i]", ip, wsIndex, "started connecting...");
+ deviceSocket.createConnection(wsIndex, ip, {
+ onOpen: (ws) => {
+ const fn = deviceConnection.createOpenHandler({
+ markDeviceStatus: (w, st) => this.markDeviceStatus(w, st),
+ sendMsg: (w, msg) => this.wsSendMsg(w, msg),
+ firstDevListRequest: this.firstDevListRequest,
+ currentPageName: this.currentPageName,
+ selectedWs: this.selectedWs,
+ sendCurrentPageNameToSelectedWs: () => this.sendCurrentPageNameToSelectedWs(),
+ });
+ fn(ws);
+ },
+ onMessage: (ws, data) => this._messageHandler(ws, data),
+ onClose: (ws) => this.markDeviceStatus(ws, false),
+ onError: (ws) => this.markDeviceStatus(ws, false),
+ });
+ }
+
+ _messageHandler(ws, data) {
+ if (typeof data === "string") {
+ if (data === "/tstr|") this.ack(ws, true);
+ return;
+ }
+ if (data instanceof Blob) {
+ if (ws === this.selectedWs) this.parseBlob(data, ws);
+ if (this.currentPageName === "/|") this.parseAllBlob(data, ws);
+ }
+ }
+
+ connectToAllDevices() {
+ deviceConnection.connectToAllDevices(
+ this.deviceList,
+ (ws) => this.getSelectedDeviceData(ws),
+ this.selectedWs,
+ (wsIndex, ip) => this._createConnection(wsIndex, ip)
+ );
+ }
+
+ _printAllCreatedWs() {
+ if (this.debug) console.log("[i]", "[ws]", "device count:", this.deviceList.length);
+ }
+
+ markDeviceStatus(ws, status) {
+ this.deviceList.forEach((device) => {
+ if (device.ws === ws) {
+ device.status = status;
+ device.ping = 0;
+ if (device.status === true) {
+ console.log("[i]", device.ip, ws, "status online");
+ } else {
+ console.log("[i]", device.ip, ws, "status offline");
+ this.deleteWidget(ws);
+ this.sortingLayout(ws);
+ }
+ }
+ });
+ this.selectedDeviceDataRefresh();
+ eventEmitter.emit("deviceListUpdated");
+ }
+
+ deleteWidget(ws) {
+ this.layoutJson = this.layoutJson.filter((item) => item.ws !== ws);
+ }
+
+ async parseBlob(blob, ws) {
+ await blobProtocol.parseBlob(blob, ws, this._blobHandlers);
+ }
+
+ async parseAllBlob(blob, ws) {
+ await blobProtocol.parseAllBlob(blob, ws, this._allBlobHandlers);
+ }
+
+ async onParced() {
+ if (this.currentPageName === "/|") {
+ this.pageReady.dash = true;
+ this._emitLayoutJsonUpdated();
+ }
+
+ if (
+ this.currentPageName === "/config|" &&
+ this.parsed.itemsJson &&
+ this.parsed.widgetsJson &&
+ this.parsed.configJson &&
+ this.parsed.settingsJson
+ ) {
+ this.clearParcedFlags();
+ this.pageReady.config = true;
+ this._emitLayoutJsonUpdated();
+ eventEmitter.emit("configUpdated", {
+ configJson: this.configJson,
+ scenarioTxt: this.scenarioTxt,
+ widgetsJson: this.widgetsJson,
+ itemsJson: this.itemsJson,
+ });
+ if (this.debug) console.log("✔✔", "config page parced");
+ }
+
+ if (
+ this.currentPageName === "/connection|" &&
+ this.parsed.ssidJson &&
+ this.parsed.settingsJson &&
+ this.parsed.errorsJson
+ ) {
+ this.clearParcedFlags();
+ if (this.debug) console.log("✔✔", "connection page parced");
+ this.pageReady.connection = true;
+ this._emitLayoutJsonUpdated();
+ eventEmitter.emit("connectionUpdated", {
+ settingsJson: this.settingsJson,
+ errorsJson: this.errorsJson,
+ ssidJson: this.ssidJson,
+ });
+ }
+
+ if (this.currentPageName === "/list|" && this.parsed.settingsJson) {
+ this.clearParcedFlags();
+ if (this.debug) console.log("✔✔", "list page parced");
+ this.pageReady.list = true;
+ this._emitLayoutJsonUpdated();
+ }
+
+ if (this.currentPageName === "/system|" && this.parsed.errorsJson && this.parsed.settingsJson) {
+ this.clearParcedFlags();
+ if (this.options.onSystemParsed) await this.options.onSystemParsed();
+ eventEmitter.emit("systemUpdated");
+ if (this.debug) console.log("✔✔", "system page parced");
+ this.pageReady.system = true;
+ this._emitLayoutJsonUpdated();
+ }
+
+ if (this.currentPageName === "/profile|" && this.parsed.flashProfileJson) {
+ this.clearParcedFlags();
+ if (this.debug) console.log("✔✔", "profile page parced");
+ this.pageReady.profile = true;
+ if (this.options.onProfileParsed) await this.options.onProfileParsed();
+ this._emitLayoutJsonUpdated();
+ }
+ }
+
+ _emitLayoutJsonUpdated() {
+ eventEmitter.emit("layoutJsonUpdated", {
+ layoutJson: this.layoutJson,
+ pages: this.pages,
+ pageReady: { ...this.pageReady },
+ });
+ }
+
+ devListOverride() {
+ this.deviceList = deviceListManager.devListOverride(this.incDeviceList);
+ console.log("[i]", "[devlist]", "devlist overrided");
+ eventEmitter.emit("deviceListUpdated");
+ }
+
+ devListCombine() {
+ this.deviceList = deviceListManager.devListCombine(this.deviceList, this.incDeviceList);
+ console.log("[i]", "[devlist]", "devlist combined");
+ eventEmitter.emit("deviceListUpdated");
+ }
+
+ combineArrays(A, B) {
+ return deviceListManager.combineArrays(A, B);
+ }
+
+ 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);
+ }
+
+ sortingLayout(ws) {
+ this.layoutJson.sort(function (a, b) {
+ if (a.descr < b.descr) return -1;
+ if (a.descr > b.descr) return 1;
+ return 0;
+ });
+ this.pages = [];
+ const newPage = Array.from(new Set(Array.from(this.layoutJson, ({ page }) => page)));
+ newPage.forEach(function (item) {
+ this.pages = [...this.pages, JSON.parse(JSON.stringify({ page: item }))];
+ }, this);
+ 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|");
+ this._emitLayoutJsonUpdated();
+ }
+
+ 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) {
+ 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|");
+ this._emitLayoutJsonUpdated();
+ }
+
+ updateWidget(newStatusJson) {
+ for (let i = 0; i < this.layoutJson.length; i++) {
+ if (this.layoutJson[i].topic === newStatusJson.topic) {
+ this.layoutJson[i] = this.jsonConcat(this.layoutJson[i], newStatusJson);
+ this.layoutJson[i].sent = false;
+ break;
+ }
+ }
+ this._emitLayoutJsonUpdated();
+ }
+
+ async apdateWidgetByArray(newStatusJson) {
+ console.log("[i]", "collecting arrays");
+ let error = true;
+ if (this.layoutJson.length > 0) {
+ for (let i = 0; i < this.layoutJson.length; i++) {
+ if (this.layoutJson[i].topic === newStatusJson.topic) {
+ error = false;
+ this.layoutJson[i] = this.jsonConcatEx(this.layoutJson[i], newStatusJson);
+ let prevArr = this.layoutJson[i].status;
+ let newArr = newStatusJson.status;
+ if (prevArr) {
+ prevArr = [...prevArr, ...newArr];
+ this.layoutJson[i].status = prevArr;
+ } else {
+ this.layoutJson[i].status = newArr;
+ }
+ this.layoutJson[i].sent = false;
+ }
+ }
+ } else {
+ console.log("[E]", "layoutJson missing");
+ }
+ if (error) console.log("[E]", "topic not found ", newStatusJson.topic);
+ this._emitLayoutJsonUpdated();
+ }
+
+ jsonConcat(o1, o2) {
+ for (const key in o2) {
+ o1[key] = o2[key];
+ }
+ return o1;
+ }
+
+ jsonConcatEx(o1, o2) {
+ for (const key in o2) {
+ if (key !== "status") o1[key] = o2[key];
+ }
+ return o1;
+ }
+
+ clearData() {
+ this.itemsJson = [];
+ this.widgetsJson = [];
+ this.configJson = [];
+ this.scenarioTxt = " ";
+ this.settingsJson = {};
+ this.errorsJson = {};
+ this.layoutJson = [];
+ this.paramsJson = {};
+ this.otaJson = {};
+ this.flashProfileJson = {};
+ for (const key of Object.keys(this.pageReady)) {
+ this.pageReady[key] = false;
+ }
+ this.clearParcedFlags();
+ if (this.debug) console.log("[i]", "all json files cleared");
+ this._emitLayoutJsonUpdated();
+ }
+
+ clearParcedFlags() {
+ console.log("[i]", "parced flags cleared");
+ for (const key of Object.keys(this.parsed)) {
+ this.parsed[key] = false;
+ }
+ }
+
+ sendCurrentPageNameToSelectedWs() {
+ if (this.selectedWs !== undefined) {
+ this.wsSendMsg(this.selectedWs, this.currentPageName);
+ }
+ }
+
+ sendToAllDevices(msg) {
+ this.deviceList.forEach((device) => {
+ if (device.status === true) this.wsSendMsg(device.ws, msg);
+ });
+ }
+
+ getSelectedDeviceData(ws) {
+ for (let i = 0; i < this.deviceList.length; i++) {
+ if (this.deviceList[i].ws === ws) {
+ this.selectedDeviceData = this.deviceList[i];
+ break;
+ }
+ }
+ }
+
+ selectedDeviceDataRefresh() {
+ this.getSelectedDeviceData(this.selectedWs);
+ this.socketConnected = this.selectedDeviceData ? this.selectedDeviceData.status : false;
+ }
+
+ wsPush(ws, topic, status) {
+ const key = topic.substring(topic.lastIndexOf("/") + 1, topic.length);
+ this.wsSendMsg(ws, "/control|" + key + "/" + status);
+ }
+
+ addCoreMsg(msg) {
+ if (this.coreMessages.length >= LOG_MAX_MESSAGES) this.coreMessages.shift();
+ this.coreMessages = [...this.coreMessages, { msg }];
+ this.coreMessages.sort(function (a, b) {
+ if (a.time > b.time) return -1;
+ if (a.time < b.time) return 1;
+ return 0;
+ });
+ }
+
+ _getInput() {
+ return {
+ name: "inputDate",
+ widget: "input",
+ size: "small",
+ color: "orange",
+ type: "date",
+ };
+ }
+
+ _modify() {
+ for (let i = 0; i < this.configJson.length; i++) {
+ delete this.configJson[i]["show"];
+ }
+ }
+
+ generateLayout() {
+ const layout = [];
+ for (let i = 0; i < this.configJson.length; i++) {
+ const config = Object.assign({}, this.configJson[i]);
+ const setWidget = config.widget;
+ let error = true;
+ for (let w = 0; w < this.widgetsJson.length; w++) {
+ if (setWidget === this.widgetsJson[w].name) {
+ const widget = Object.assign({}, this.widgetsJson[w]);
+ widget.page = config.page;
+ widget.descr = config.descr;
+ widget.topic =
+ this.settingsJson.mqttPrefix + "/" + this.settingsJson.id + "/" + config.id;
+ if (setWidget !== "nil") layout.push(widget);
+ if (widget.widget === "chart" && widget.type !== "bar") {
+ const input = this._getInput();
+ input.page = config.page;
+ input.topic =
+ this.settingsJson.mqttPrefix +
+ "/" +
+ this.settingsJson.id +
+ "/" +
+ config.id +
+ "-date";
+ input.descr = config.descr;
+ layout.push(input);
+ }
+ error = false;
+ break;
+ } else {
+ error = true;
+ }
+ }
+ if (error) console.log("[E]", "error, widget not found: " + setWidget);
+ }
+ layout.sort(function (a, b) {
+ if (a.descr < b.descr) return -1;
+ if (a.descr > b.descr) return 1;
+ return 0;
+ });
+ for (let i = 0; i < layout.length; i++) {
+ layout[i].order = i;
+ }
+ return layout;
+ }
+
+ jsonArrWrite(jsonArr, idKey, idValue, paramKey, paramValue) {
+ for (let i = 0; i < jsonArr.length; i++) {
+ const obj = jsonArr[i];
+ for (const [key, value] of Object.entries(obj)) {
+ if (key === idKey && value === idValue) {
+ obj[paramKey] = paramValue;
+ break;
+ }
+ }
+ }
+ }
+
+ saveConfig() {
+ this.wsSendMsg(this.selectedWs, "/tuoyal|" + JSON.stringify(this.generateLayout()));
+ this._modify();
+ this.wsSendMsg(this.selectedWs, "/gifnoc|" + JSON.stringify(this.configJson));
+ this.wsSendMsg(this.selectedWs, "/oiranecs|" + this.scenarioTxt);
+ this.clearData();
+ this.sendCurrentPageNameToSelectedWs();
+ }
+
+ saveSett() {
+ const size = Object.keys(this.settingsJson).length;
+ console.log("[i]", "settingsJson length: " + size);
+ if (size > 5) {
+ this.jsonArrWrite(
+ this.deviceList,
+ "ip",
+ this.getIP(this.selectedWs),
+ "name",
+ this.settingsJson.name
+ );
+ this.wsSendMsg(this.selectedWs, "/sgnittes|" + JSON.stringify(this.settingsJson));
+ eventEmitter.emit("deviceListUpdated");
+ } else {
+ window.alert("Ошибка размера settingsJson (возможно не был передан странице)");
+ }
+ this.clearData();
+ this.sendCurrentPageNameToSelectedWs();
+ }
+
+ saveList() {
+ const devListForSave = Object.assign([], this.deviceList);
+ for (let i = 0; i < devListForSave.length; i++) {
+ devListForSave[i].status = false;
+ }
+ this.wsSendMsg(this.selectedWs, "/tsil|" + JSON.stringify(devListForSave));
+ }
+
+ cleanLogs() {
+ this.wsSendMsg(this.selectedWs, "/clean|");
+ }
+
+ saveMqtt() {
+ const size = Object.keys(this.settingsJson).length;
+ this.wsSendMsg(this.selectedWs, "/tuoyal|" + JSON.stringify(this.generateLayout()));
+ if (size > 5) {
+ this.wsSendMsg(this.selectedWs, "/sgnittes|" + JSON.stringify(this.settingsJson));
+ } else {
+ window.alert("Ошибка");
+ }
+ this.clearData();
+ this.wsSendMsg(this.selectedWs, "/mqtt|");
+ }
+
+ ssidClick() {
+ this.wsSendMsg(this.selectedWs, "/scan|");
+ }
+
+ rebootEsp() {
+ this.rebootOrUpdateProcess = true;
+ if (this.debug) console.log("[i]", "reboot...");
+ this.wsSendMsg(this.selectedWs, "/reboot|");
+ this.markDeviceStatus(this.selectedWs, false);
+ this.showAwaitingCircle = true;
+ this.socketConnected = false;
+ this.reconnectTimeout = 10;
+ this.remainingTimeout = this.reconnectTimeout;
+ }
+
+ updateBuild(path) {
+ this.rebootOrUpdateProcess = true;
+ console.log(path);
+ this.wsSendMsg(this.selectedWs, "/update|" + path);
+ this.showAwaitingCircle = true;
+ this.socketConnected = false;
+ this.reconnectTimeout = 20;
+ this.remainingTimeout = this.reconnectTimeout;
+ }
+
+ applicationReboot() {
+ console.log("[i]", "reboot svelte...");
+ for (const key of Object.keys(this.pageReady)) {
+ this.pageReady[key] = false;
+ }
+ this.showAwaitingCircle = true;
+ setTimeout(() => {
+ location.reload();
+ }, 1000);
+ }
+
+ cancelAlarm(alarmKey) {
+ console.log("[x]", alarmKey);
+ this.errorsJson[alarmKey] = 0;
+ this.wsSendMsg(this.selectedWs, '/rorre|{"' + alarmKey + '":0}');
+ }
+
+ moduleOrder(id, key, value) {
+ const json = { id, key, value };
+ this.wsSendMsg(this.selectedWs, "/order|" + JSON.stringify(json));
+ }
+
+ addDevInList() {
+ if (
+ this.newDevice.name !== undefined &&
+ this.newDevice.ip !== undefined &&
+ this.newDevice.id !== undefined
+ ) {
+ this.newDevice.status = false;
+ this.newDevice.ws = this.deviceList.length;
+ this.incDeviceList.push(this.newDevice);
+ this.devListCombine();
+ this.connectToAllDevices();
+ if (this.debug) console.log("[i]", "selected device: ", this.selectedDeviceData);
+ return true;
+ }
+ if (this.debug) console.log("[e]", "wrong data");
+ return false;
+ }
+
+ startReconnectTask() {
+ this.wsTestMsgTask();
+ }
+}
diff --git a/src/pages/Config.svelte b/src/pages/Config.svelte
index 525660f..1d46c3b 100644
--- a/src/pages/Config.svelte
+++ b/src/pages/Config.svelte
@@ -221,7 +221,7 @@
if (res.ok && res.data && res.data.result) {
console.log(res.data.result);
if (res.data.result.acknowledged) {
- window.open("https://portal.iotmanager.org/configs?id=" + res.data.result.insertedId + "&token=" + JWT, "_blank");
+ window.open(portal.BASE + "/configs?id=" + res.data.result.insertedId + "&token=" + JWT, "_blank");
}
errors = [{ msg: "ok_success" }];
} else if (res.errors) {
diff --git a/src/pages/Profile.svelte b/src/pages/Profile.svelte
index 3eb07e3..8cc0e9a 100644
--- a/src/pages/Profile.svelte
+++ b/src/pages/Profile.svelte
@@ -197,7 +197,7 @@
{#if build.status.build === 2 && build.status.preparation === 2 && build.status.fs === 2}
- updateBuild("http://portal.iotmanager.org/compiler/userdata/builds/" + build.orderId)} class="tbl-bdy-lg ipt-lg w-full cursor-pointer select-none bg-green-100 hover:bg-green-200">
+ | updateBuild(portal.BASE + "/compiler/userdata/builds/" + build.orderId)} class="tbl-bdy-lg ipt-lg w-full cursor-pointer select-none bg-green-100 hover:bg-green-200">
Установить
|
{:else}
diff --git a/src/pages/System.svelte b/src/pages/System.svelte
index 3b3f093..1592eed 100644
--- a/src/pages/System.svelte
+++ b/src/pages/System.svelte
@@ -131,8 +131,8 @@
export let errorsJson;
- //export let versionsList;
- //export let choosingVersion;
+ export let versionsList = {};
+ export let choosingVersion;
export let coreMessages;
export let settingsJson;
@@ -147,7 +147,10 @@
let reboot = false;
- //export let cancelAlarm = (alarmKey) => {};
+ export let cancelAlarm = (alarmKey) => {};
+
+ // Referenced by parent (App); used in commented-out template blocks (versions dropdown, alarm close)
+ $: __propsRef = { versionsList, choosingVersion, cancelAlarm };
{#if show}