diff --git a/.cursor/plans/IoTManagerWeb.code-workspace b/.cursor/plans/IoTManagerWeb.code-workspace
new file mode 100644
index 0000000..e280649
--- /dev/null
+++ b/.cursor/plans/IoTManagerWeb.code-workspace
@@ -0,0 +1,14 @@
+{
+ "folders": [
+ {
+ "path": "../.."
+ },
+ {
+ "path": "../../../IoTManager"
+ },
+ {
+ "path": "../../../IoTManagerWeb2"
+ }
+ ],
+ "settings": {}
+}
\ No newline at end of file
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/.gitignore b/.gitignore
index ff9546b..802e790 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@
.venv-mock/
.DS_Store
+_backup_refactor/
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..bd2d2ab
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,38 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Run frontend (dev)",
+ "type": "shell",
+ "command": "npm run dev",
+ "options": {
+ "cwd": "${workspaceFolder}",
+ "env": {}
+ },
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ },
+ "presentation": {
+ "reveal": "always",
+ "panel": "new"
+ },
+ "problemMatcher": []
+ },
+ {
+ "label": "Run scripts (mock backend)",
+ "type": "shell",
+ "command": "if [ -d .venv-mock ]; then .venv-mock/bin/python scripts/mock_backend.py --host 0.0.0.0 --ws-port 81 --http-port 8081; else python3 -m venv .venv-mock && .venv-mock/bin/pip install -r scripts/requirements-mock.txt && .venv-mock/bin/python scripts/mock_backend.py --host 0.0.0.0 --ws-port 81 --http-port 8081; fi",
+ "options": {
+ "cwd": "${workspaceFolder}",
+ "env": {}
+ },
+ "group": "build",
+ "presentation": {
+ "reveal": "always",
+ "panel": "new"
+ },
+ "problemMatcher": []
+ }
+ ]
+}
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/scripts/mock_backend.py b/scripts/mock_backend.py
index 989cb1c..2387ccf 100644
--- a/scripts/mock_backend.py
+++ b/scripts/mock_backend.py
@@ -112,6 +112,43 @@ def get_layout(device_slot: int) -> list:
# Per-slot state for controls; keys = last segment of topic (id). Merged into get_params.
_params_state: dict = {} # slot -> { topic_id: value }
+def _topic_id(topic: str) -> str:
+ """Last segment of topic (e.g. /mock/d0/relay1 -> relay1)."""
+ if not topic or "/" not in topic:
+ return topic or ""
+ return topic.rstrip("/").split("/")[-1]
+
+
+def _ensure_params_for_layout(slot: int, layout: list) -> None:
+ """Add missing topic ids from layout to _params_state[slot] so added widgets get data."""
+ state = _get_params_state(slot)
+ for w in layout:
+ topic = w.get("topic") or ""
+ wid = _topic_id(topic)
+ if not wid or wid in state:
+ continue
+ widget_type = (w.get("widget") or "").lower()
+ if "chart" in widget_type:
+ state[wid] = ""
+ elif "toggle" in widget_type:
+ state[wid] = "0"
+ elif "range" in widget_type:
+ state[wid] = str(w.get("min", 0) if isinstance(w.get("min"), (int, float)) else 50)
+ elif "input" in widget_type:
+ itype = w.get("type") or "number"
+ if itype == "number":
+ state[wid] = "20"
+ elif itype == "text":
+ state[wid] = "Room"
+ elif itype == "time":
+ state[wid] = "08:00"
+ elif itype == "date":
+ state[wid] = "2025-01-01"
+ else:
+ state[wid] = "0"
+ else:
+ state[wid] = "0"
+
def _get_params_state(slot: int) -> dict:
if slot not in _params_state:
@@ -126,9 +163,12 @@ def _get_params_state(slot: int) -> dict:
return _params_state[slot]
-def get_params(device_slot: int) -> dict:
- """Params JSON: topic id -> value (for layout widgets)."""
- return dict(_get_params_state(device_slot))
+def get_params(device_slot: int, layout: Optional[list] = None) -> dict:
+ """Params JSON: topic id -> value (for layout widgets). If layout given, ensure all its topic ids have state."""
+ state = _get_params_state(device_slot)
+ if layout:
+ _ensure_params_for_layout(device_slot, layout)
+ return dict(state)
def get_chart_data(topic: str, points: int = 20, base_val: float = 20.0, amplitude: float = 5.0) -> dict:
@@ -151,7 +191,7 @@ _settings_store: dict = {} # slot -> dict
def get_settings(http_host: str, http_port: int, slot: int = 0) -> dict:
"""Settings JSON; serverip must point to mock HTTP for ver.json. System page needs timezone, wg, log, mqttin, i2c, pinSCL, pinSDA, i2cFreq."""
base = {
- "name": "MockDevice",
+ "name": f"MockDevice-{slot}",
"apssid": "IoTmanager",
"appass": "",
"routerssid": "",
@@ -182,23 +222,25 @@ def get_settings(http_host: str, http_port: int, slot: int = 0) -> dict:
return base
-def get_errors() -> dict:
- """Errors JSON for System page: bn, bt, bver, wver, timenow, upt, uptm, uptw, rssi, heap, freeBytes, fl, rst."""
+def get_errors(slot: int = 0) -> dict:
+ """Errors JSON for System page (per device): bn, bt, bver, wver, timenow, upt, uptm, uptw, rssi, heap, freeBytes, fl, rst."""
import time
+ ts = int(time.time())
+ # Different system info per device slot so UI shows which device is selected
return {
- "bn": "esp32",
- "bt": "2024-01-01 12:00",
- "bver": "1.0.0",
- "wver": "4.2.0",
- "timenow": str(int(time.time())),
- "upt": "1d 02:30",
- "uptm": "1d 02:30",
- "uptw": "1d 02:30",
- "rssi": 5,
- "heap": "120000",
- "freeBytes": "2.1M",
- "fl": "1024",
- "rst": "Software reset",
+ "bn": f"esp32-d{slot}",
+ "bt": f"2024-0{1 + slot}-0{1 + slot} 12:00",
+ "bver": f"1.0.{slot}",
+ "wver": f"4.2.{slot}",
+ "timenow": str(ts),
+ "upt": f"{1 + slot}d 0{2 + slot}:{30 + slot * 5}",
+ "uptm": f"{1 + slot}d 0{2 + slot}:{30 + slot * 5}",
+ "uptw": f"{1 + slot}d 0{2 + slot}:{30 + slot * 5}",
+ "rssi": -65 + slot * 10,
+ "heap": str(120000 - slot * 10000),
+ "freeBytes": f"{2 - slot * 0.2:.1f}M",
+ "fl": str(1024 + slot * 512),
+ "rst": "Software reset" if slot == 0 else "Power-on reset",
}
@@ -239,15 +281,41 @@ def get_devlist(host: str) -> list:
# Saved device list from /tsil| (reversed "list"). When udps=0 backend returns from "file", else from "heap" (default).
_saved_devlist: Optional[list] = None
+# Configurator: saved layout/config/scenario per slot (from /tuoyal|, /gifnoc|, /oiranecs|)
+_saved_layout: dict = {} # slot -> list (layout JSON array)
+_saved_config: dict = {} # slot -> list (config JSON array)
+_saved_scenario: dict = {} # slot -> str (scenario text)
def get_items_json() -> list:
- """Minimal items list."""
+ """Items list covering all widget types (anydata, chart, toggle, range, input)."""
return [
{"name": "Выберите элемент", "num": 0},
{"header": "virtual_elments"},
- {"global": 0, "name": "Temp", "type": "Reading", "subtype": "AnalogAdc", "id": "temp", "widget": "anydataTmp", "page": "Сенсоры", "descr": "Temperature", "num": 1},
- {"global": 0, "name": "Graph", "type": "Writing", "subtype": "Loging", "id": "log", "widget": "chart2", "page": "Графики 1", "descr": "Temperature", "num": 2, "points": 100},
+ # anydata
+ {"global": 0, "name": "Text", "type": "Reading", "subtype": "Variable", "id": "text", "widget": "anydataDef", "page": "Сенсоры", "descr": "Text", "num": 1},
+ {"global": 0, "name": "Temperature", "type": "Reading", "subtype": "AnalogAdc", "id": "temp", "widget": "anydataTmp", "page": "Сенсоры", "descr": "Temperature", "num": 2},
+ {"global": 0, "name": "Humidity", "type": "Reading", "subtype": "Variable", "id": "hum", "widget": "anydataDef", "page": "Сенсоры", "descr": "Humidity", "num": 3},
+ {"global": 0, "name": "Pressure", "type": "Reading", "subtype": "Variable", "id": "pressure", "widget": "anydataDef", "page": "Сенсоры", "descr": "Pressure", "num": 4},
+ {"global": 0, "name": "Voltage", "type": "Reading", "subtype": "Variable", "id": "voltage", "widget": "anydataDef", "page": "Сенсоры", "descr": "Voltage", "num": 5},
+ {"global": 0, "name": "Power", "type": "Reading", "subtype": "Variable", "id": "power", "widget": "anydataDef", "page": "Сенсоры", "descr": "Power", "num": 6},
+ {"global": 0, "name": "RSSI", "type": "Reading", "subtype": "Variable", "id": "rssi", "widget": "anydataDef", "page": "Сенсоры", "descr": "WiFi RSSI", "num": 7},
+ # chart
+ {"global": 0, "name": "Chart line", "type": "Writing", "subtype": "Loging", "id": "log", "widget": "chart2", "page": "Графики 1", "descr": "Chart", "num": 10, "points": 100},
+ {"global": 0, "name": "Chart bar", "type": "Writing", "subtype": "Loging", "id": "logBar", "widget": "chartBar", "page": "Графики 2", "descr": "Bar chart", "num": 11, "points": 100},
+ # toggle
+ {"global": 0, "name": "Toggle", "type": "Writing", "subtype": "ButtonOut", "id": "relay", "widget": "toggleDef", "page": "Реле и свет", "descr": "Relay", "num": 20},
+ {"global": 0, "name": "Light", "type": "Writing", "subtype": "ButtonOut", "id": "light", "widget": "toggleDef", "page": "Реле и свет", "descr": "Light", "num": 21},
+ {"global": 0, "name": "Fan", "type": "Writing", "subtype": "ButtonOut", "id": "fan", "widget": "toggleDef", "page": "Реле и свет", "descr": "Fan", "num": 22},
+ # range
+ {"global": 0, "name": "Dimmer", "type": "Writing", "subtype": "Variable", "id": "dimmer", "widget": "rangeDef", "page": "Параметры", "descr": "Dimmer", "num": 30},
+ {"global": 0, "name": "Volume", "type": "Writing", "subtype": "Variable", "id": "volume", "widget": "rangeDef", "page": "Параметры", "descr": "Volume", "num": 31},
+ {"global": 0, "name": "Setpoint", "type": "Writing", "subtype": "Variable", "id": "setpoint", "widget": "rangeDef", "page": "Параметры", "descr": "Setpoint", "num": 32},
+ # input
+ {"global": 0, "name": "Input number", "type": "Reading", "subtype": "Variable", "id": "settemp", "widget": "inputNumber", "page": "Параметры", "descr": "Number", "num": 40},
+ {"global": 0, "name": "Input text", "type": "Reading", "subtype": "Variable", "id": "label", "widget": "inputText", "page": "Параметры", "descr": "Label", "num": 41},
+ {"global": 0, "name": "Input time", "type": "Reading", "subtype": "Variable", "id": "alarmtime", "widget": "inputTime", "page": "Параметры", "descr": "Time", "num": 42},
+ {"global": 0, "name": "Input date", "type": "Reading", "subtype": "Variable", "id": "eventdate", "widget": "inputDate", "page": "Параметры", "descr": "Date", "num": 43},
]
@@ -303,11 +371,12 @@ def assign_slot() -> int:
async def handle_ws_message(ws, message: str, slot: int) -> None:
"""Handle text command from frontend and send responses."""
- global _saved_devlist
+ global _saved_devlist, _saved_layout, _saved_config, _saved_scenario
if "|" not in message:
return
cmd = message.split("|")[0] + "|"
+ # Heartbeat/ping: same as original IoTManager WsServer.cpp — reply immediately so frontend can measure RTT (device.ping)
if cmd == "/tst|":
await ws.send("/tstr|")
return
@@ -320,19 +389,20 @@ async def handle_ws_message(ws, message: str, slot: int) -> None:
return ws.send(make_binary_frame(header, payload))
if cmd == "/|":
- layout = get_layout(slot)
+ layout = _saved_layout.get(slot) if slot in _saved_layout else get_layout(slot)
await send_bin("layout", json.dumps(layout))
- # Send params immediately so front has data (front also requests /params| after layout; both trigger updateAllStatuses)
- await send_bin("params", json.dumps(get_params(slot)))
+ # Send params immediately so front has data (incl. for saved-layout widgets)
+ await send_bin("params", json.dumps(get_params(slot, layout)))
return
if cmd == "/params|":
- params = get_params(slot)
+ layout = _saved_layout.get(slot) if slot in _saved_layout else get_layout(slot)
+ params = get_params(slot, layout)
await send_bin("params", json.dumps(params))
return
if cmd == "/charts|":
- layout = get_layout(slot)
+ layout = _saved_layout.get(slot) if slot in _saved_layout else get_layout(slot)
for w in layout:
if w.get("widget") != "chart" or not w.get("topic"):
continue
@@ -350,8 +420,10 @@ async def handle_ws_message(ws, message: str, slot: int) -> None:
if cmd == "/config|":
await send_bin("itemsj", json.dumps(get_items_json()))
await send_bin("widget", json.dumps(get_widgets_json()))
- await send_bin("config", json.dumps(get_config_json()))
- await send_bin("scenar", "// mock scenario\n")
+ config_data = _saved_config[slot] if slot in _saved_config else get_config_json()
+ await send_bin("config", json.dumps(config_data))
+ scenario_txt = _saved_scenario.get(slot, "// mock scenario\n")
+ await send_bin("scenar", scenario_txt if isinstance(scenario_txt, str) else "// mock scenario\n")
await send_bin("settin", json.dumps(get_settings(HTTP_HOST, HTTP_PORT, slot)))
return
@@ -360,7 +432,7 @@ async def handle_ws_message(ws, message: str, slot: int) -> None:
await send_bin("config", json.dumps(get_config_json()))
await send_bin("settin", json.dumps(get_settings(HTTP_HOST, HTTP_PORT, slot)))
await send_bin("ssidli", json.dumps(get_ssid_list()))
- await send_bin("errors", json.dumps(get_errors()))
+ await send_bin("errors", json.dumps(get_errors(slot)))
return
if cmd == "/list|":
@@ -380,12 +452,12 @@ async def handle_ws_message(ws, message: str, slot: int) -> None:
return
if cmd == "/system|":
- await send_bin("errors", json.dumps(get_errors()))
+ await send_bin("errors", json.dumps(get_errors(slot)))
await send_bin("settin", json.dumps(get_settings(HTTP_HOST, HTTP_PORT, slot)))
return
if cmd == "/dev|":
- await send_bin("errors", json.dumps(get_errors()))
+ await send_bin("errors", json.dumps(get_errors(slot)))
await send_bin("settin", json.dumps(get_settings(HTTP_HOST, HTTP_PORT, slot)))
await send_bin("config", json.dumps(get_config_json()))
await send_bin("itemsj", json.dumps(get_items_json()))
@@ -414,7 +486,7 @@ async def handle_ws_message(ws, message: str, slot: int) -> None:
try:
data = json.loads(payload)
_settings_store[slot] = data
- await send_bin("errors", json.dumps(get_errors()))
+ await send_bin("errors", json.dumps(get_errors(slot)))
except json.JSONDecodeError:
pass
return
@@ -440,8 +512,34 @@ async def handle_ws_message(ws, message: str, slot: int) -> None:
# Reboot (no-op in mock)
return
- # Save commands: no-op
- if cmd in ("/gifnoc|", "/tuoyal|", "/oiranecs|"):
+ # Save configurator data (layout, config, scenario) per slot
+ if cmd == "/tuoyal|":
+ payload = message[len(cmd) :].strip()
+ if payload:
+ try:
+ data = json.loads(payload)
+ if isinstance(data, list):
+ _saved_layout[slot] = data
+ _ensure_params_for_layout(slot, data)
+ print(f"[mock] layout saved for slot {slot}, {len(data)} widget(s)")
+ except json.JSONDecodeError:
+ pass
+ return
+ if cmd == "/gifnoc|":
+ payload = message[len(cmd) :].strip()
+ if payload:
+ try:
+ data = json.loads(payload)
+ if isinstance(data, list):
+ _saved_config[slot] = data
+ print(f"[mock] config saved for slot {slot}, {len(data)} item(s)")
+ except json.JSONDecodeError:
+ pass
+ return
+ if cmd == "/oiranecs|":
+ payload = message[len(cmd) :].strip()
+ _saved_scenario[slot] = payload if payload is not None else ""
+ print(f"[mock] scenario saved for slot {slot}")
return
diff --git a/src/App.svelte b/src/App.svelte
index 32c2b8b..c48a944 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,50 +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 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";
- import CloudIcon from "./svg/Cloud.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,
@@ -80,1329 +48,199 @@
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,
- },
- ];
-
- var ackTimeoutsArr = [];
- var startMillis = [];
- var ping = [];
-
- 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
- let socket = [];
- let socketConnected = false;
- let selectedDeviceData = undefined;
+ let ssidJson = {};
+ let versionsList = {};
+ let choosingVersion = undefined;
let selectedWs = 0;
- let originalWs = 0;
+ let socketConnected = false;
+ let percent = 0;
+ let remainingTimeout = 60;
- let newDevice = {};
- let coreMessages = [];
+ let opened = true;
+ let preventMove = false;
+ let screenSize;
+ let showInput = false;
- //***********************************************************navigation********************************************************/
- let currentPageName = undefined;
+ $: currentPageName = wsManager.currentPageName;
+ $: 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 () => {
- try {
- const JWT = Cookies.get("token_iotm2");
- let res = await fetch("https://portal.iotmanager.org/api/user/email", {
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${JWT}`,
- },
- mode: "cors",
- method: "GET",
- });
- if (res.ok) {
- userdata = await res.json();
- serverOnline = true;
- } else {
- console.log("error", res.statusText);
- serverOnline = true;
- }
- } catch (e) {
- console.log("error", e);
- serverOnline = false;
- }
- };
-
- //****************************************************web sockets section******************************************************/
- function connectToAllDevices() {
- getSelectedDeviceData(selectedWs);
- for (let i = 0; i < deviceList.length; i++) {
- deviceList[i].ws = i;
- if (deviceList[i].status === false || deviceList[i].status === undefined) {
- wsConnect(i);
- wsEventAdd(i);
- }
- }
- }
-
- function printAllCreatedWs() {
- if (socket) {
- for (let i = 0; i < socket.length; i++) {
- if (debug) console.log("[i]", "[ws]", "WebSocket client No: ", i);
- }
- }
- }
-
- 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);
- }
-
- function wsConnect(ws) {
- let ip = getIP(ws);
- if (ip === "error") {
- if (debug) console.log("[e]", "device list wrong");
- } else {
- socket[ws] = new WebSocket("ws://" + ip + ":81");
- socket.binaryType = "blob";
- if (debug) console.log("[i]", ip, ws, "started connecting...");
- }
- }
-
- function getIP(ws) {
- let ret = "error";
- deviceList.forEach((device) => {
- if (ws === device.ws) {
- ret = device.ip;
- }
- });
- return ret;
- }
-
- function wsEventAdd(ws) {
- if (socket[ws]) {
- let ip = getIP(ws);
- socket[ws].addEventListener("open", function (event) {
- //if (debug) console.log("[i]", ip, ws, "completed connecting");
-
- markDeviceStatus(ws, true);
- //при первом подключении запросим список устройств
- if (firstDevListRequest && ws === 0) wsSendMsg(ws, "/devlist|");
- //при подключении отправляем название страницы
- if (currentPageName === "/|") {
- //всем устройствам
- wsSendMsg(ws, currentPageName);
- } else {
- //только выбранному
- if (ws === selectedWs) {
- sendCurrentPageNameToSelectedWs();
- }
- }
- });
-
- //события веб сокетов
- socket[ws].addEventListener("message", function (event) {
- if (typeof event.data === "string") {
- let data = event.data;
- if (data === "/tstr|") {
- //прилетело подтверждение значит устройство онлайн
-
- ack(ws, true);
- }
- }
- if (event.data instanceof Blob) {
- //принимаем данные только для выбранного устройства
- if (ws === selectedWs) {
- parseBlob(event.data, ws);
- }
- //собираем данные со всех устройств только в случае если пользователь на dashboard
- if (currentPageName === "/|") {
- parseAllBlob(event.data, ws);
- }
- }
- });
- socket[ws].addEventListener("close", (event) => {
- if (debug) console.log("[e]", ip, "connection closed");
- //socket[ws].close();
- markDeviceStatus(ws, false);
- });
- socket[ws].addEventListener("error", function (event) {
- if (debug) console.log("[e]", ip, "connection error");
- //socket[ws].close();
- markDeviceStatus(ws, false);
- });
- } else {
- if (debug) console.log("[e]", "socket not exist");
- }
- }
-
- async function parseBlob(blob, ws) {
- //получаем заголовок
- var blobHeader = blob.slice(0, 6);
- let header = await blobHeader.text();
- //console.log("header: ", header);
- //получаем размер
- var blobSize = blob.slice(7, 11);
- let size = await blobSize.text();
- //console.log("size: ", size);
-
- if (header === "itemsj") {
- let out = {};
- if (await getPayloadAsJson(blob, size, out)) {
- itemsJson = out.json;
- parsed.itemsJson = true;
- if (blobDebug) console.log("[✔]", "itemsJson: ", itemsJson);
- } else {
- parsed.itemsJson = false;
- if (blobDebug) console.log("[e]", "itemsJson parse error");
- }
- }
- if (header === "widget") {
- let out = {};
- if (await getPayloadAsJson(blob, size, out)) {
- widgetsJson = out.json;
- parsed.widgetsJson = true;
- if (blobDebug) console.log("[✔]", "widgetsJson: ", widgetsJson);
- } else {
- parsed.widgetsJson = false;
- if (blobDebug) console.log("[e]", "widgetsJson parse error");
- }
- }
- if (header === "config") {
- let out = {};
- if (await getPayloadAsJson(blob, size, out)) {
- configJson = out.json;
- parsed.configJson = true;
- if (blobDebug) console.log("[✔]", "configJson: ", configJson);
- } else {
- parsed.configJson = false;
- if (blobDebug) console.log("[e]", "configJson parse error");
- }
- }
- if (header === "scenar") {
- scenarioTxt = await getPayloadAsTxt(blob, size);
- if (blobDebug) console.log("[i]", "scenarioTxt: ", scenarioTxt);
- }
- if (header === "settin") {
- let out = {};
- if (await getPayloadAsJson(blob, size, out)) {
- settingsJson = out.json;
- parsed.settingsJson = true;
- if (blobDebug) console.log("[✔]", "settingsJson: ", settingsJson);
- } else {
- parsed.settingsJson = false;
- if (blobDebug) console.log("[e]", "settingsJson parse error");
- }
- }
- if (header === "ssidli") {
- let out = {};
- if (await getPayloadAsJson(blob, size, out)) {
- ssidJson = out.json;
- parsed.ssidJson = true;
- if (blobDebug) console.log("[✔]", "ssidJson: ", ssidJson);
- } else {
- parsed.ssidJson = false;
- if (blobDebug) console.log("[e]", "ssidJson parse error");
- }
- }
- //прием данных об ошибках
- if (header === "errors") {
- let out = {};
- if (await getPayloadAsJson(blob, size, out)) {
- errorsJson = out.json;
- parsed.errorsJson = true;
- if (blobDebug) console.log("[✔]", "errorsJson: ", errorsJson);
- } else {
- parsed.errorsJson = false;
- if (blobDebug) console.log("[e]", "errorsJson parse error");
- }
- }
- //приход списка устройств
- if (header === "devlis") {
- let out = {};
- if (await getPayloadAsJson(blob, size, out)) {
- incDeviceList = [];
- incDeviceList = out.json;
- parsed.incDeviceList = true;
- if (blobDebug) console.log("[✔]", "incDeviceList: ", incDeviceList);
- await initDevList();
- } else {
- parsed.incDeviceList = false;
- if (blobDebug) console.log("[e]", "incDeviceList parse error");
- }
- }
- //приход профиля
- if (header === "prfile") {
- let out = {};
- if (await getPayloadAsJson(blob, size, out)) {
- flashProfileJson = out.json;
- parsed.flashProfileJson = true;
- if (blobDebug) console.log("[✔]", "flashProfileJson: ", flashProfileJson);
- } else {
- parsed.flashProfileJson = false;
- if (blobDebug) console.log("[e]", "flashProfileJson parse error");
- }
- }
-
- if (header === "otaupd") {
- let out = {};
- if (await getPayloadAsJson(blob, size, out)) {
- otaJson = out.json;
- parsed.otaJson = true;
- if (blobDebug) console.log("[✔]", "otaJson: ", otaJson);
- } else {
- parsed.otaJson = false;
- if (blobDebug) console.log("[e]", "otaJson parse error");
- }
- }
-
- if (header === "corelg") {
- let txt = await getPayloadAsTxt(blob, size);
- //console.log("[--]", ws, txt);
- addCoreMsg(txt);
- }
-
- await onParced();
- }
-
- async function parseAllBlob(blob, ws) {
- //получаем заголовок
- var blobHeader = blob.slice(0, 6);
- let header = await blobHeader.text();
- //console.log("header: ", header);
- //получаем размер
- var blobSize = blob.slice(7, 11);
- let size = await blobSize.text();
- //console.log("size: ", size);
-
- if (header === "status") {
- let out = {};
- if (await getPayloadAsJson(blob, size, out)) {
- let statusJson = out.json;
- updateWidget(statusJson);
- //if (blobDebug)
- console.log("[✔]", "statusJson: ", statusJson);
- } else {
- if (blobDebug) console.log("[e]", "statusJson parse error");
- }
- }
- if (header === "layout") {
- let out = {};
- if (await getPayloadAsJson(blob, size, out)) {
- let devLayout = out.json;
- combineLayoutsInOne(ws, devLayout);
- if (blobDebug) console.log("[✔]", "devLayout: ", devLayout);
- } else {
- if (blobDebug) console.log("[e]", "devLayout parse error");
- }
- }
- if (header === "params") {
- let out = {};
- if (await getPayloadAsJson(blob, size, out)) {
- let devParams = out.json;
- paramsJson = {
- ...paramsJson,
- ...devParams,
- };
- paramsJson = paramsJson;
- updateAllStatuses(ws);
- onParced();
- if (blobDebug) console.log("[✔]", "devParams: ", devParams);
- } else {
- if (blobDebug) console.log("[e]", "devParams parse error");
- }
- }
- if (header === "charta") {
- let txt = await getPayloadAsTxt(blob, size);
- txt = "[" + txt.substring(0, txt.length - 1) + "]";
- let chartJson;
- try {
- chartJson = JSON.parse(txt);
- if (blobDebug) console.log("[i]", "chart data json: ", chartJson);
- } catch (e) {
- if (blobDebug) console.log("[e]", "chart json data parce error, return");
- return;
- }
- let out = {};
- let addJson = {};
- if (await getJsonAsJson(blob, size, out)) {
- addJson = out.json;
- if (blobDebug) console.log("[i]", "chart add json: ", addJson);
- } else {
- if (blobDebug) console.log("[e]", "chart json add-ns parce error, return");
- return;
- }
- let finalDataJson = {};
- finalDataJson.status = chartJson;
-
- finalDataJson = {
- ...finalDataJson,
- ...addJson,
- };
- if (blobDebug) console.log("[✔]", "chartJson: ", finalDataJson);
- apdateWidgetByArray(finalDataJson);
- }
- if (header === "chartb") {
- let out = {};
- if (await getPayloadAsJson(blob, size, out)) {
- let status = out.json;
- apdateWidgetByArray(status);
- if (blobDebug) console.log("[✔]", "chart status: ", status);
- } else {
- if (blobDebug) console.log("[e]", "status parse error");
- }
- }
- }
-
- async function getJsonAsJson(blob, size, out) {
- let partBlob = blob.slice(12, size);
- let txt = await partBlob.text();
- try {
- out.json = JSON.parse(txt);
- out.parse = true;
- } catch (e) {
- if (debug) console.log("[e]", "json parce error: ", txt);
- out.parse = false;
- }
- return out.parse;
- }
-
- async function getPayloadAsJson(blob, size, out) {
- let partBlob = blob.slice(size, blob.length);
- let txt = await partBlob.text();
- try {
- out.json = JSON.parse(txt);
- out.parse = true;
- } catch (e) {
- if (debug) console.log("[e]", "json parse error: ", txt);
- out.parse = false;
- }
- return out.parse;
- }
-
- async function getPayloadAsTxt(blob, size) {
- let txtBlob = blob.slice(size, blob.length);
- let txt = await txtBlob.text();
- return txt;
- }
-
- 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 () => {
- try {
- let res = await fetch("https://portal.iotmanager.org/compiler/allmodinfo", {
- mode: "cors",
- method: "GET",
- });
- if (res.ok) {
- allmodeinfo = await res.json();
- allmodeinfo = allmodeinfo.message;
- } else {
- console.log("error", res.statusText);
- }
- } catch (e) {
- console.log("error", e);
- }
- };
-
- const getProfile = async () => {
- try {
- const JWT = Cookies.get("token_iotm2");
- let res = await fetch("https://portal.iotmanager.org/compiler/profile", {
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${JWT}`,
- },
- mode: "cors",
- method: "GET",
- });
- if (res.ok) {
- profile = await res.json();
- profile = profile.message;
- await markProfileAsPerThisDevProfile();
- } else {
- console.log("error", res.statusText);
- }
- } catch (e) {
- console.log("error", e);
- }
- };
-
- 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;
- }
- });
- }
- });
- }
- };
-
- async function initDevList() {
- if (firstDevListRequest) {
- //при первом запросе листа устройств запишем его целеком
- devListOverride();
- } else {
- //при последующих прилетах списка устройств мы переписываем в массиве только то что изменилось
- devListCombine();
- }
- firstDevListRequest = false;
- deviceList = deviceList;
- parsed.deviceListJson = true;
- if (blobDebug) console.log("[✔]", "deviceList parced");
- onParced();
- selectedDeviceDataRefresh();
- //затем подключимся к всему полученному списку устройств
- connectToAllDevices();
- }
-
- //перезапись листа устройств
- async function devListOverride() {
- deviceList = incDeviceList;
- sortList(deviceList);
- //установим заведомо статус уже присутствующего устроства в true для того что бы предотвратить повторное подключение!!!
- deviceList[0].status = true;
- console.log("[i]", "[devlist]", "devlist overrided");
- }
-
- //добавление только новых элементов в лист устройств (если такого ip не было)
- async function devListCombine() {
- deviceList = combineArrays(deviceList, incDeviceList);
- sortList(deviceList);
- console.log("[i]", "[devlist]", "devlist combined");
- }
-
- function sortList(list) {
- let firstDev = list.shift();
- list.sort(function (a, b) {
- if (a.name < b.name) {
- return -1;
- }
- if (a.name > b.name) {
- return 1;
- }
- return 0;
- });
- list.unshift(firstDev);
- }
-
- //***********************************************************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);
- }
-
- function scale(number, inMin, inMax, outMin, outMax) {
- return ((number - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
- }
-
- //тикер который тикает каждую секунду и на каждую 0 секунду запускает проверку соединения
- function wsTestMsgTask() {
- tickerTask = setTimeout(wsTestMsgTask, 1000);
- if (!preventReconnect) {
- remainingTimeout--;
- if (rebootOrUpdateProcess && socketConnected) {
- rebootOrUpdateProcess = false;
- showAwaitingCircle = false;
- reconnectTimeout = 60;
- remainingTimeout = reconnectTimeout;
- }
- percent = scale(remainingTimeout, reconnectTimeout, 0, 0, 100);
- if (remainingTimeout <= 0) {
- if (debug) console.log("[i]", "----timer tick----");
- printAllCreatedWs();
- remainingTimeout = reconnectTimeout;
- deviceList.forEach((device) => {
- if (device.status === false || device.status === undefined) {
- wsConnect(device.ws);
- wsEventAdd(device.ws);
- } else {
- wsSendMsg(device.ws, "/tst|");
- ack(device.ws, false);
- }
- });
- }
- }
- }
-
- function ack(ws, st) {
- if (!st) {
- startMillis[ws] = Date.now();
- ackTimeoutsArr[ws] = setTimeout(function () {
- markDeviceStatus(ws, false);
- }, waitingAckTimeout);
- } else {
- if (ackTimeoutsArr[ws]) clearTimeout(ackTimeoutsArr[ws]);
- if (startMillis[ws]) {
- ping[ws] = Date.now() - startMillis[ws];
- }
- for (let i = 0; i < deviceList.length; i++) {
- if (deviceList[i].ws === ws) {
- deviceList[i].ping = ping[ws];
- }
- }
- deviceList = deviceList;
- }
- }
-
- function wsSendMsg(ws, msg) {
- if (socket[ws] && socket[ws].readyState === 1) {
- socket[ws].send(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");
- }
- }
-
- 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 combineArrays(A, B) {
- var ids = new Set(A.map((d) => d.ip));
- let output = [...A, ...B.filter((d) => !ids.has(d.ip))];
- return output;
- }
-
- //всякий раз когда список устройств был обновлен
- 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 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;
- } else {
- if (debug) console.log("[e]", "wrong data");
- return false;
- }
- }
- }
-
- 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;
- }
- }
- }
- }
-
- //**********************************************************modal*************************************************************************/
function onCheck() {
- if (screenSize < 900) {
- preventMove = true;
+ preventMove = screenSize < 900;
+ }
+
+ 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 {
- preventMove = false;
+ wsManager.serverOnline = res.serverOnline !== 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 = {};
- if (settingsJson.serverip) {
- try {
- let url = settingsJson.serverip + "/iotm/ver.json";
- console.log("url", url);
- let res = await fetch(url, {
- mode: "cors",
- method: "GET",
- });
- if (res.ok) {
- versionsList = await res.json();
- versionsList = versionsList[errorsJson.bn];
- choosingVersion = errorsJson.bver;
- console.log(JSON.stringify(versionsList));
- } else {
- choosingVersion = undefined;
- console.log("error, versions list not received", res.statusText);
- }
- } catch (e) {
- choosingVersion = undefined;
- console.log("error, versions list not received");
- console.log(e);
+ 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 {
+ wsManager.choosingVersion = undefined;
}
- } else {
- console.log("error, server missing");
- }
- }
-
- 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));
- }
+
+ 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;
+ });
+ });
+ }
+ }
+ }
+ };
+
+ onCheck();
+ opened = screenSize > 900;
+ wsManager.firstDevListRequest = true;
+ wsManager.selectedDeviceDataRefresh();
+ socketConnected = wsManager.socketConnected;
+ percent = wsManager.percent;
+ remainingTimeout = wsManager.remainingTimeout;
+ wsManager.connectToAllDevices();
+ wsManager.startReconnectTask();
+
+ eventEmitter.on("layoutJsonUpdated", (data) => {
+ layoutJson = data.layoutJson || [];
+ pages = data.pages || [];
+ if (data.pageReady) pageReady = data.pageReady;
+ if (data.configJson !== undefined) configJson = data.configJson;
+ if (data.scenarioTxt !== undefined) scenarioTxt = data.scenarioTxt;
+ });
+ eventEmitter.on("deviceListUpdated", () => {
+ deviceList = [...wsManager.deviceList];
+ socketConnected = wsManager.socketConnected;
+ });
+ eventEmitter.on("reconnectTick", (data) => {
+ percent = data.percent;
+ remainingTimeout = data.remainingTimeout;
+ });
+ 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={wsManager.userdata} />
- {#if !socketConnected && currentPageName != "/|"}
+ {#if !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} 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}
-
+