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} - - -
-
- {#if showDropdown} -
- -
- {/if} - - - - -
- -
-
-
- - + + onCheck()} userdata={wsManager.userdata} />
-
-
Developed by Dmitry Borisenko
-
+