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} />