mirror of
https://github.com/IoTManagerProject/IoTManagerWeb.git
synced 2026-06-10 12:39:28 +03:00
14
.cursor/plans/IoTManagerWeb.code-workspace
Normal file
14
.cursor/plans/IoTManagerWeb.code-workspace
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "../.."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../../IoTManager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../../IoTManagerWeb2"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
174
.cursor/plans/websocketmanager_refactor_d29d54ab.plan.md
Normal file
174
.cursor/plans/websocketmanager_refactor_d29d54ab.plan.md
Normal file
@@ -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 через события.
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@
|
|||||||
.venv-mock/
|
.venv-mock/
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
_backup_refactor/
|
||||||
|
|||||||
38
.vscode/tasks.json
vendored
Normal file
38
.vscode/tasks.json
vendored
Normal file
@@ -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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
16
SMOKE_TEST.md
Normal file
16
SMOKE_TEST.md
Normal file
@@ -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.
|
||||||
@@ -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.
|
# Per-slot state for controls; keys = last segment of topic (id). Merged into get_params.
|
||||||
_params_state: dict = {} # slot -> { topic_id: value }
|
_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:
|
def _get_params_state(slot: int) -> dict:
|
||||||
if slot not in _params_state:
|
if slot not in _params_state:
|
||||||
@@ -126,9 +163,12 @@ def _get_params_state(slot: int) -> dict:
|
|||||||
return _params_state[slot]
|
return _params_state[slot]
|
||||||
|
|
||||||
|
|
||||||
def get_params(device_slot: int) -> dict:
|
def get_params(device_slot: int, layout: Optional[list] = None) -> dict:
|
||||||
"""Params JSON: topic id -> value (for layout widgets)."""
|
"""Params JSON: topic id -> value (for layout widgets). If layout given, ensure all its topic ids have state."""
|
||||||
return dict(_get_params_state(device_slot))
|
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:
|
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:
|
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."""
|
"""Settings JSON; serverip must point to mock HTTP for ver.json. System page needs timezone, wg, log, mqttin, i2c, pinSCL, pinSDA, i2cFreq."""
|
||||||
base = {
|
base = {
|
||||||
"name": "MockDevice",
|
"name": f"MockDevice-{slot}",
|
||||||
"apssid": "IoTmanager",
|
"apssid": "IoTmanager",
|
||||||
"appass": "",
|
"appass": "",
|
||||||
"routerssid": "",
|
"routerssid": "",
|
||||||
@@ -182,23 +222,25 @@ def get_settings(http_host: str, http_port: int, slot: int = 0) -> dict:
|
|||||||
return base
|
return base
|
||||||
|
|
||||||
|
|
||||||
def get_errors() -> dict:
|
def get_errors(slot: int = 0) -> dict:
|
||||||
"""Errors JSON for System page: bn, bt, bver, wver, timenow, upt, uptm, uptw, rssi, heap, freeBytes, fl, rst."""
|
"""Errors JSON for System page (per device): bn, bt, bver, wver, timenow, upt, uptm, uptw, rssi, heap, freeBytes, fl, rst."""
|
||||||
import time
|
import time
|
||||||
|
ts = int(time.time())
|
||||||
|
# Different system info per device slot so UI shows which device is selected
|
||||||
return {
|
return {
|
||||||
"bn": "esp32",
|
"bn": f"esp32-d{slot}",
|
||||||
"bt": "2024-01-01 12:00",
|
"bt": f"2024-0{1 + slot}-0{1 + slot} 12:00",
|
||||||
"bver": "1.0.0",
|
"bver": f"1.0.{slot}",
|
||||||
"wver": "4.2.0",
|
"wver": f"4.2.{slot}",
|
||||||
"timenow": str(int(time.time())),
|
"timenow": str(ts),
|
||||||
"upt": "1d 02:30",
|
"upt": f"{1 + slot}d 0{2 + slot}:{30 + slot * 5}",
|
||||||
"uptm": "1d 02:30",
|
"uptm": f"{1 + slot}d 0{2 + slot}:{30 + slot * 5}",
|
||||||
"uptw": "1d 02:30",
|
"uptw": f"{1 + slot}d 0{2 + slot}:{30 + slot * 5}",
|
||||||
"rssi": 5,
|
"rssi": -65 + slot * 10,
|
||||||
"heap": "120000",
|
"heap": str(120000 - slot * 10000),
|
||||||
"freeBytes": "2.1M",
|
"freeBytes": f"{2 - slot * 0.2:.1f}M",
|
||||||
"fl": "1024",
|
"fl": str(1024 + slot * 512),
|
||||||
"rst": "Software reset",
|
"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 device list from /tsil| (reversed "list"). When udps=0 backend returns from "file", else from "heap" (default).
|
||||||
_saved_devlist: Optional[list] = None
|
_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:
|
def get_items_json() -> list:
|
||||||
"""Minimal items list."""
|
"""Items list covering all widget types (anydata, chart, toggle, range, input)."""
|
||||||
return [
|
return [
|
||||||
{"name": "Выберите элемент", "num": 0},
|
{"name": "Выберите элемент", "num": 0},
|
||||||
{"header": "virtual_elments"},
|
{"header": "virtual_elments"},
|
||||||
{"global": 0, "name": "Temp", "type": "Reading", "subtype": "AnalogAdc", "id": "temp", "widget": "anydataTmp", "page": "Сенсоры", "descr": "Temperature", "num": 1},
|
# anydata
|
||||||
{"global": 0, "name": "Graph", "type": "Writing", "subtype": "Loging", "id": "log", "widget": "chart2", "page": "Графики 1", "descr": "Temperature", "num": 2, "points": 100},
|
{"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:
|
async def handle_ws_message(ws, message: str, slot: int) -> None:
|
||||||
"""Handle text command from frontend and send responses."""
|
"""Handle text command from frontend and send responses."""
|
||||||
global _saved_devlist
|
global _saved_devlist, _saved_layout, _saved_config, _saved_scenario
|
||||||
if "|" not in message:
|
if "|" not in message:
|
||||||
return
|
return
|
||||||
cmd = message.split("|")[0] + "|"
|
cmd = message.split("|")[0] + "|"
|
||||||
|
|
||||||
|
# Heartbeat/ping: same as original IoTManager WsServer.cpp — reply immediately so frontend can measure RTT (device.ping)
|
||||||
if cmd == "/tst|":
|
if cmd == "/tst|":
|
||||||
await ws.send("/tstr|")
|
await ws.send("/tstr|")
|
||||||
return
|
return
|
||||||
@@ -320,19 +389,20 @@ async def handle_ws_message(ws, message: str, slot: int) -> None:
|
|||||||
return ws.send(make_binary_frame(header, payload))
|
return ws.send(make_binary_frame(header, payload))
|
||||||
|
|
||||||
if cmd == "/|":
|
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))
|
await send_bin("layout", json.dumps(layout))
|
||||||
# Send params immediately so front has data (front also requests /params| after layout; both trigger updateAllStatuses)
|
# Send params immediately so front has data (incl. for saved-layout widgets)
|
||||||
await send_bin("params", json.dumps(get_params(slot)))
|
await send_bin("params", json.dumps(get_params(slot, layout)))
|
||||||
return
|
return
|
||||||
|
|
||||||
if cmd == "/params|":
|
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))
|
await send_bin("params", json.dumps(params))
|
||||||
return
|
return
|
||||||
|
|
||||||
if cmd == "/charts|":
|
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:
|
for w in layout:
|
||||||
if w.get("widget") != "chart" or not w.get("topic"):
|
if w.get("widget") != "chart" or not w.get("topic"):
|
||||||
continue
|
continue
|
||||||
@@ -350,8 +420,10 @@ async def handle_ws_message(ws, message: str, slot: int) -> None:
|
|||||||
if cmd == "/config|":
|
if cmd == "/config|":
|
||||||
await send_bin("itemsj", json.dumps(get_items_json()))
|
await send_bin("itemsj", json.dumps(get_items_json()))
|
||||||
await send_bin("widget", json.dumps(get_widgets_json()))
|
await send_bin("widget", json.dumps(get_widgets_json()))
|
||||||
await send_bin("config", json.dumps(get_config_json()))
|
config_data = _saved_config[slot] if slot in _saved_config else get_config_json()
|
||||||
await send_bin("scenar", "// mock scenario\n")
|
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)))
|
await send_bin("settin", json.dumps(get_settings(HTTP_HOST, HTTP_PORT, slot)))
|
||||||
return
|
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("config", json.dumps(get_config_json()))
|
||||||
await send_bin("settin", json.dumps(get_settings(HTTP_HOST, HTTP_PORT, slot)))
|
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("ssidli", json.dumps(get_ssid_list()))
|
||||||
await send_bin("errors", json.dumps(get_errors()))
|
await send_bin("errors", json.dumps(get_errors(slot)))
|
||||||
return
|
return
|
||||||
|
|
||||||
if cmd == "/list|":
|
if cmd == "/list|":
|
||||||
@@ -380,12 +452,12 @@ async def handle_ws_message(ws, message: str, slot: int) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if cmd == "/system|":
|
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)))
|
await send_bin("settin", json.dumps(get_settings(HTTP_HOST, HTTP_PORT, slot)))
|
||||||
return
|
return
|
||||||
|
|
||||||
if cmd == "/dev|":
|
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("settin", json.dumps(get_settings(HTTP_HOST, HTTP_PORT, slot)))
|
||||||
await send_bin("config", json.dumps(get_config_json()))
|
await send_bin("config", json.dumps(get_config_json()))
|
||||||
await send_bin("itemsj", json.dumps(get_items_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:
|
try:
|
||||||
data = json.loads(payload)
|
data = json.loads(payload)
|
||||||
_settings_store[slot] = data
|
_settings_store[slot] = data
|
||||||
await send_bin("errors", json.dumps(get_errors()))
|
await send_bin("errors", json.dumps(get_errors(slot)))
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
pass
|
pass
|
||||||
return
|
return
|
||||||
@@ -440,8 +512,34 @@ async def handle_ws_message(ws, message: str, slot: int) -> None:
|
|||||||
# Reboot (no-op in mock)
|
# Reboot (no-op in mock)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Save commands: no-op
|
# Save configurator data (layout, config, scenario) per slot
|
||||||
if cmd in ("/gifnoc|", "/tuoyal|", "/oiranecs|"):
|
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
|
return
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
1448
src/App.svelte
1448
src/App.svelte
File diff suppressed because it is too large
Load Diff
75
src/api/deviceSocket.js
Normal file
75
src/api/deviceSocket.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* WebSocket pool for devices: create by wsIndex, events via callbacks.
|
||||||
|
* No parsing or reconnect logic — App provides callbacks.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const sockets = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create WebSocket for device at wsIndex, bind events to callbacks.
|
||||||
|
* @param {number} wsIndex - device index (same as device.ws)
|
||||||
|
* @param {string} ip - device IP
|
||||||
|
* @param {object} callbacks - { onOpen(ws), onMessage(ws, data), onClose(ws), onError(ws) }
|
||||||
|
*/
|
||||||
|
export function createConnection(wsIndex, ip, callbacks) {
|
||||||
|
const url = "ws://" + ip + ":81";
|
||||||
|
const socket = new WebSocket(url);
|
||||||
|
socket.binaryType = "blob"; // fix: set on instance, not on array
|
||||||
|
sockets.set(wsIndex, socket);
|
||||||
|
|
||||||
|
socket.addEventListener("open", () => {
|
||||||
|
if (callbacks.onOpen) callbacks.onOpen(wsIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.addEventListener("message", (event) => {
|
||||||
|
if (callbacks.onMessage) callbacks.onMessage(wsIndex, event.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.addEventListener("close", () => {
|
||||||
|
if (callbacks.onClose) callbacks.onClose(wsIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.addEventListener("error", () => {
|
||||||
|
if (callbacks.onError) callbacks.onError(wsIndex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send message if socket exists and is open.
|
||||||
|
* @param {number} wsIndex
|
||||||
|
* @param {string} msg
|
||||||
|
*/
|
||||||
|
export function send(wsIndex, msg) {
|
||||||
|
const socket = sockets.get(wsIndex);
|
||||||
|
if (socket && socket.readyState === 1) {
|
||||||
|
socket.send(msg);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} wsIndex
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isOpen(wsIndex) {
|
||||||
|
const socket = sockets.get(wsIndex);
|
||||||
|
return !!(socket && socket.readyState === 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get socket instance (for compatibility / debug). Prefer send/isOpen.
|
||||||
|
* @param {number} wsIndex
|
||||||
|
* @returns {WebSocket|undefined}
|
||||||
|
*/
|
||||||
|
export function getSocket(wsIndex) {
|
||||||
|
return sockets.get(wsIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove socket from pool (e.g. on close if needed). Does not close the socket.
|
||||||
|
* @param {number} wsIndex
|
||||||
|
*/
|
||||||
|
export function removeSocket(wsIndex) {
|
||||||
|
sockets.delete(wsIndex);
|
||||||
|
}
|
||||||
28
src/api/firmware.js
Normal file
28
src/api/firmware.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Firmware / OTA: load ver.json from device-configured server.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { get } from "./http.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load ver.json from serverip/iotm/ver.json.
|
||||||
|
* @param {string} serverip - base URL (e.g. from settingsJson.serverip)
|
||||||
|
* @returns {Promise<{ ok: boolean, data?: object }>} data is full ver.json
|
||||||
|
*/
|
||||||
|
export async function getVersionsList(serverip) {
|
||||||
|
if (!serverip) {
|
||||||
|
console.log("error", "server missing");
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const url = serverip + "/iotm/ver.json";
|
||||||
|
const res = await get(url);
|
||||||
|
if (res.ok) {
|
||||||
|
return { ok: true, data: res.data };
|
||||||
|
}
|
||||||
|
return { ok: false };
|
||||||
|
} catch (e) {
|
||||||
|
console.log("error", "versions list not received", e);
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/api/http.js
Normal file
63
src/api/http.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* Generic fetch wrapper: JSON, errors, headers.
|
||||||
|
* Returns { ok, data, status, statusText } or throws on network error.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const DEFAULT_OPTIONS = {
|
||||||
|
mode: "cors",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} url
|
||||||
|
* @param {RequestInit} options
|
||||||
|
* @returns {Promise<{ ok: boolean, data?: any, status: number, statusText: string }>}
|
||||||
|
*/
|
||||||
|
export async function request(url, options = {}) {
|
||||||
|
const opts = {
|
||||||
|
...DEFAULT_OPTIONS,
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...DEFAULT_OPTIONS.headers,
|
||||||
|
...(options.headers || {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const res = await fetch(url, opts);
|
||||||
|
let data;
|
||||||
|
const contentType = res.headers.get("content-type");
|
||||||
|
if (contentType && contentType.includes("application/json")) {
|
||||||
|
try {
|
||||||
|
data = await res.json();
|
||||||
|
} catch {
|
||||||
|
data = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data = await res.text();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: res.ok,
|
||||||
|
data,
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET request, returns parsed JSON when possible.
|
||||||
|
*/
|
||||||
|
export async function get(url, headers = {}) {
|
||||||
|
return request(url, { method: "GET", headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST request with JSON body.
|
||||||
|
*/
|
||||||
|
export async function post(url, body, headers = {}) {
|
||||||
|
return request(url, {
|
||||||
|
method: "POST",
|
||||||
|
body: typeof body === "string" ? body : JSON.stringify(body || {}),
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
173
src/api/portal.js
Normal file
173
src/api/portal.js
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
/**
|
||||||
|
* Portal API: https://portal.iotmanager.org
|
||||||
|
* Auth/user, configurations, compiler (profile, orders, etc.).
|
||||||
|
*
|
||||||
|
* CORS: If you see "Redirect is not allowed for a preflight request", the server
|
||||||
|
* is redirecting OPTIONS requests (e.g. to HTTPS or login). Fix on server:
|
||||||
|
* do not redirect preflight; return 200 with CORS headers for OPTIONS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { get, post } from "./http.js";
|
||||||
|
|
||||||
|
/** Portal base URL; export for building links in UI (e.g. configs, build install). */
|
||||||
|
export const BASE = "https://portal.iotmanager.org";
|
||||||
|
|
||||||
|
function authHeaders(token) {
|
||||||
|
return {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/user/email — current user by JWT.
|
||||||
|
* @param {string} JWT
|
||||||
|
* @returns {Promise<{ ok: boolean, userdata?: object, serverOnline: boolean }>}
|
||||||
|
*/
|
||||||
|
export async function getUser(JWT) {
|
||||||
|
try {
|
||||||
|
const res = await get(BASE + "/api/user/email", authHeaders(JWT));
|
||||||
|
if (res.ok) {
|
||||||
|
return { ok: true, userdata: res.data, serverOnline: true };
|
||||||
|
}
|
||||||
|
return { ok: false, serverOnline: true };
|
||||||
|
} catch (e) {
|
||||||
|
console.log("error", e);
|
||||||
|
return { ok: false, serverOnline: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/auth/login — login, returns token in content.message.
|
||||||
|
* @param {object} user { username, password }
|
||||||
|
* @returns {Promise<{ ok: boolean, data: object }>}
|
||||||
|
*/
|
||||||
|
export async function login(user) {
|
||||||
|
const res = await post(BASE + "/api/auth/login", user);
|
||||||
|
return { ok: res.ok, data: res.data };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /compiler/allmodinfo — all module info.
|
||||||
|
* @returns {Promise<{ ok: boolean, allmodeinfo?: object }>}
|
||||||
|
*/
|
||||||
|
export async function getModInfo() {
|
||||||
|
try {
|
||||||
|
const res = await get(BASE + "/compiler/allmodinfo", { "Content-Type": "application/json" });
|
||||||
|
if (res.ok && res.data && res.data.message) {
|
||||||
|
return { ok: true, allmodeinfo: res.data.message };
|
||||||
|
}
|
||||||
|
return { ok: false };
|
||||||
|
} catch (e) {
|
||||||
|
console.log("error", e);
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /compiler/profile — user compiler profile (JWT).
|
||||||
|
* @param {string} JWT
|
||||||
|
* @returns {Promise<{ ok: boolean, profile?: object }>}
|
||||||
|
*/
|
||||||
|
export async function getProfile(JWT) {
|
||||||
|
try {
|
||||||
|
const res = await get(BASE + "/compiler/profile", authHeaders(JWT));
|
||||||
|
if (res.ok && res.data && res.data.message) {
|
||||||
|
return { ok: true, profile: res.data.message };
|
||||||
|
}
|
||||||
|
return { ok: false };
|
||||||
|
} catch (e) {
|
||||||
|
console.log("error", e);
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/configurations/get — list configurations (JWT).
|
||||||
|
* @param {string} JWT
|
||||||
|
* @returns {Promise<{ ok: boolean, configurations?: array }>}
|
||||||
|
*/
|
||||||
|
export async function getConfigurations(JWT) {
|
||||||
|
try {
|
||||||
|
const res = await get(BASE + "/api/configurations/get", authHeaders(JWT));
|
||||||
|
if (res.ok) {
|
||||||
|
return { ok: true, configurations: res.data };
|
||||||
|
}
|
||||||
|
return { ok: false };
|
||||||
|
} catch (e) {
|
||||||
|
console.log("error", e);
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/configurations/add — publish config (JWT, body: category, topic, text, config, scenario, gallery, type, username).
|
||||||
|
* @param {string} JWT
|
||||||
|
* @param {object} body
|
||||||
|
* @returns {Promise<{ ok: boolean, data?: object, errors?: array }>}
|
||||||
|
*/
|
||||||
|
export async function addConfiguration(JWT, body) {
|
||||||
|
try {
|
||||||
|
const res = await post(BASE + "/api/configurations/add", body, authHeaders(JWT));
|
||||||
|
if (res.ok && res.data) {
|
||||||
|
return { ok: true, data: res.data };
|
||||||
|
}
|
||||||
|
return { ok: false, errors: res.data?.message };
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /compiler/userorders — user build orders (JWT).
|
||||||
|
* @param {string} JWT
|
||||||
|
* @returns {Promise<{ ok: boolean, userBuilds?: array }>}
|
||||||
|
*/
|
||||||
|
export async function getUserOrders(JWT) {
|
||||||
|
try {
|
||||||
|
const res = await get(BASE + "/compiler/userorders", authHeaders(JWT));
|
||||||
|
if (res.ok) {
|
||||||
|
return { ok: true, userBuilds: res.data };
|
||||||
|
}
|
||||||
|
return { ok: false };
|
||||||
|
} catch (e) {
|
||||||
|
console.log("error", e);
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /compiler/delete/builds/:orderId — delete build (JWT).
|
||||||
|
* @param {string} JWT
|
||||||
|
* @param {string} orderId
|
||||||
|
* @returns {Promise<{ ok: boolean }>}
|
||||||
|
*/
|
||||||
|
export async function deleteBuild(JWT, orderId) {
|
||||||
|
try {
|
||||||
|
const res = await get(BASE + "/compiler/delete/builds/" + orderId, authHeaders(JWT));
|
||||||
|
return { ok: res.ok };
|
||||||
|
} catch (e) {
|
||||||
|
console.log("error", e);
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /compiler/order — place compiler order (JWT, body: profile with username).
|
||||||
|
* @param {string} JWT
|
||||||
|
* @param {object} profile
|
||||||
|
* @returns {Promise<{ ok: boolean, data?: object, errors?: array }>}
|
||||||
|
*/
|
||||||
|
export async function placeOrder(JWT, profile) {
|
||||||
|
try {
|
||||||
|
const res = await post(BASE + "/compiler/order", profile, authHeaders(JWT));
|
||||||
|
if (res.ok) {
|
||||||
|
return { ok: true, data: res.data };
|
||||||
|
}
|
||||||
|
return { ok: false, errors: res.data?.message };
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<script>
|
|
||||||
export let show = true;
|
|
||||||
export let header = "header";
|
|
||||||
export let text = "text";
|
|
||||||
let onlyCloseButton = true;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="modal">
|
|
||||||
{#if show}
|
|
||||||
<!-- This example requires Tailwind CSS v2.0+ -->
|
|
||||||
<div class="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
|
||||||
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
|
||||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true" />
|
|
||||||
<!-- This element is to trick the browser into centering the modal contents. -->
|
|
||||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
|
||||||
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
|
||||||
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
|
||||||
<div class="sm:flex sm:items-start">
|
|
||||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
|
||||||
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">{header}</h3>
|
|
||||||
<div class="mt-2">
|
|
||||||
<p class="text-sm text-gray-500">{text}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
|
||||||
{#if onlyCloseButton}
|
|
||||||
<button type="button" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"> Deactivate </button>
|
|
||||||
{/if}
|
|
||||||
<button on:click={() => (show = false)} type="button" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">Закрыть</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<script>
|
|
||||||
export let show = true;
|
|
||||||
export let pass;
|
|
||||||
export let checkPassword = (pass) => {};
|
|
||||||
|
|
||||||
import Card from "../components/Card.svelte";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="modal">
|
|
||||||
{#if show}
|
|
||||||
<!-- This example requires Tailwind CSS v2.0+ -->
|
|
||||||
<div class="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
|
||||||
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
|
||||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true" />
|
|
||||||
<!-- This element is to trick the browser into centering the modal contents. -->
|
|
||||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
|
||||||
<div class="inline-block align-bottom bg-white rounded-lg overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
|
||||||
<Card>
|
|
||||||
<div class="flex items-center mb-4">
|
|
||||||
<div class="w-1/2">
|
|
||||||
<h2 class="pr-4 text-gray-500 font-bold text-xxl truncate">Введите пароль</h2>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-center w-1/2">
|
|
||||||
<input bind:value={pass} class="ipt-rnd text-center focus:border-indigo-500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="grd-2col1">
|
|
||||||
<button class="btn-lg">{"Закрыть"}</button>
|
|
||||||
<button class="btn-lg" on:click={() => checkPassword(pass)}>{"Вход"}</button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
3
src/components/layout/AppFooter.svelte
Normal file
3
src/components/layout/AppFooter.svelte
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<footer class="h-4 bg-gray-100 border-gray-300 shadow-lg">
|
||||||
|
<div class="flex justify-center content-center text-xxs text-gray-500">Developed by Dmitry Borisenko</div>
|
||||||
|
</footer>
|
||||||
28
src/components/layout/AppHeader.svelte
Normal file
28
src/components/layout/AppHeader.svelte
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<script>
|
||||||
|
import CloudIcon from "../../svg/Cloud.svelte";
|
||||||
|
|
||||||
|
export let deviceList = [];
|
||||||
|
export let selectedWs = 0;
|
||||||
|
export let showDropdown = true;
|
||||||
|
export let socketConnected = false;
|
||||||
|
export let devicesDropdownChange = () => {};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header class="h-10 w-full bg-gray-100 overflow-auto shadow-md">
|
||||||
|
<div class="flex content-center items-center justify-end">
|
||||||
|
{#if showDropdown}
|
||||||
|
<div class="px-15 py-1">
|
||||||
|
<select class="border border-indigo-500 border-1" bind:value={selectedWs} on:change={() => devicesDropdownChange()}>
|
||||||
|
{#each deviceList as device}
|
||||||
|
<option value={device.ws}>
|
||||||
|
{device.name}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="pl-4 pr-4 py-1">
|
||||||
|
<CloudIcon color={socketConnected === true ? "text-green-500" : "text-red-500"} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
41
src/components/layout/AppNav.svelte
Normal file
41
src/components/layout/AppNav.svelte
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<script>
|
||||||
|
export let opened = true;
|
||||||
|
export let onCheck = () => {};
|
||||||
|
export let userdata = null;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav class="flex">
|
||||||
|
<input class="w-0 h-0" bind:checked={opened} on:change={() => onCheck()} id="menu__toggle" type="checkbox" />
|
||||||
|
|
||||||
|
<label class="menu__btn" for="menu__toggle">
|
||||||
|
<span />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<ul class="menu__box">
|
||||||
|
<li>
|
||||||
|
<a class="menu__item" href="/">{"Управление"}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="menu__item" href="/config">{"Конфигуратор"}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="menu__item" href="/connection">{"Подключение"}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="menu__item" href="/system">{"Системные"}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="menu__item" href="/list">{"Устройства"}</a>
|
||||||
|
</li>
|
||||||
|
{#if userdata}
|
||||||
|
<li>
|
||||||
|
<a class="menu__item" href="/profile">{"Модули"}</a>
|
||||||
|
</li>
|
||||||
|
{:else}
|
||||||
|
<li>
|
||||||
|
<a class="menu__item" href="/login">{"Вход"}</a>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
<li class="flex flex-col pl-6 pt-3 w-full h-screen"></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
32
src/eventEmitter.js
Normal file
32
src/eventEmitter.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Simple event bus for UI notifications (layoutJsonUpdated, deviceListUpdated).
|
||||||
|
* Used by WebSocketManager to notify App.svelte without tight coupling.
|
||||||
|
*/
|
||||||
|
class EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
this._listeners = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
on(event, listener) {
|
||||||
|
if (!this._listeners[event]) this._listeners[event] = [];
|
||||||
|
this._listeners[event].push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
off(event, listener) {
|
||||||
|
if (!this._listeners[event]) return;
|
||||||
|
this._listeners[event] = this._listeners[event].filter((l) => l !== listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(event, ...args) {
|
||||||
|
if (!this._listeners[event]) return;
|
||||||
|
this._listeners[event].forEach((l) => {
|
||||||
|
try {
|
||||||
|
l(...args);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[eventEmitter]", event, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const eventEmitter = new EventEmitter();
|
||||||
656
src/lib/WebSocketManager.js
Normal file
656
src/lib/WebSocketManager.js
Normal file
@@ -0,0 +1,656 @@
|
|||||||
|
/**
|
||||||
|
* WebSocketManager: owns device/WS state and delegates to api/ and lib/.
|
||||||
|
* Emits layoutJsonUpdated and deviceListUpdated for App.svelte reactivity.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as deviceSocket from "../api/deviceSocket.js";
|
||||||
|
import * as deviceConnection from "./deviceConnection.js";
|
||||||
|
import * as deviceListManager from "./deviceListManager.js";
|
||||||
|
import * as blobProtocol from "./blobProtocol.js";
|
||||||
|
import * as wsReconnect from "./wsReconnect.js";
|
||||||
|
import { eventEmitter } from "../eventEmitter.js";
|
||||||
|
|
||||||
|
const LOG_MAX_MESSAGES = 100;
|
||||||
|
const WAITING_ACK_TIMEOUT = 18000;
|
||||||
|
const DEFAULT_RECONNECT_TIMEOUT = 60;
|
||||||
|
const debug = true;
|
||||||
|
|
||||||
|
export default class WebSocketManager {
|
||||||
|
constructor(initialDeviceList, options = {}) {
|
||||||
|
this.options = options;
|
||||||
|
this.debug = options.debug !== false && debug;
|
||||||
|
|
||||||
|
// State (mirror of App.svelte)
|
||||||
|
this.deviceList = Array.isArray(initialDeviceList) ? [...initialDeviceList] : [];
|
||||||
|
this.layoutJson = [];
|
||||||
|
this.paramsJson = {};
|
||||||
|
this.pages = [];
|
||||||
|
this.parsed = {
|
||||||
|
itemsJson: false,
|
||||||
|
widgetsJson: false,
|
||||||
|
configJson: false,
|
||||||
|
scenarioTxt: false,
|
||||||
|
settingsJson: false,
|
||||||
|
ssidJson: false,
|
||||||
|
incDeviceList: false,
|
||||||
|
deviceListJson: false,
|
||||||
|
errorsJson: false,
|
||||||
|
statusJson: false,
|
||||||
|
paramsJson: false,
|
||||||
|
flashProfileJson: false,
|
||||||
|
otaJson: false,
|
||||||
|
};
|
||||||
|
this.pageReady = {
|
||||||
|
dash: false,
|
||||||
|
config: false,
|
||||||
|
connection: false,
|
||||||
|
list: false,
|
||||||
|
system: false,
|
||||||
|
dev: false,
|
||||||
|
profile: false,
|
||||||
|
};
|
||||||
|
this.firstDevListRequest = true;
|
||||||
|
this.currentPageName = undefined;
|
||||||
|
this.selectedWs = 0;
|
||||||
|
this.selectedDeviceData = undefined;
|
||||||
|
this.socketConnected = false;
|
||||||
|
this.originalWs = 0;
|
||||||
|
this.itemsJson = [];
|
||||||
|
this.widgetsJson = [];
|
||||||
|
this.configJson = [];
|
||||||
|
this.scenarioTxt = " ";
|
||||||
|
this.settingsJson = {};
|
||||||
|
this.ssidJson = {};
|
||||||
|
this.errorsJson = {};
|
||||||
|
this.incDeviceList = [];
|
||||||
|
this.flashProfileJson = {};
|
||||||
|
this.otaJson = {};
|
||||||
|
this.newDevice = {};
|
||||||
|
this.coreMessages = [];
|
||||||
|
this.reconnectTimeout = DEFAULT_RECONNECT_TIMEOUT;
|
||||||
|
this.remainingTimeout = this.reconnectTimeout;
|
||||||
|
this.preventReconnect = false;
|
||||||
|
this.rebootOrUpdateProcess = false;
|
||||||
|
this.showAwaitingCircle = false;
|
||||||
|
this.percent = 0;
|
||||||
|
this.userdata = null;
|
||||||
|
this.serverOnline = false;
|
||||||
|
this.allmodeinfo = null;
|
||||||
|
this.profile = null;
|
||||||
|
this.versionsList = {};
|
||||||
|
this.choosingVersion = undefined;
|
||||||
|
|
||||||
|
// Build blob handlers that call this.*
|
||||||
|
this._blobHandlers = {
|
||||||
|
setItemsJson: (v) => (this.itemsJson = v),
|
||||||
|
setParsedItemsJson: (v) => (this.parsed.itemsJson = v),
|
||||||
|
setWidgetsJson: (v) => (this.widgetsJson = v),
|
||||||
|
setParsedWidgetsJson: (v) => (this.parsed.widgetsJson = v),
|
||||||
|
setConfigJson: (v) => (this.configJson = v),
|
||||||
|
setParsedConfigJson: (v) => (this.parsed.configJson = v),
|
||||||
|
setScenarioTxt: (v) => (this.scenarioTxt = v),
|
||||||
|
setSettingsJson: (v) => (this.settingsJson = v),
|
||||||
|
setParsedSettingsJson: (v) => (this.parsed.settingsJson = v),
|
||||||
|
setSsidJson: (v) => (this.ssidJson = v),
|
||||||
|
setParsedSsidJson: (v) => (this.parsed.ssidJson = v),
|
||||||
|
setErrorsJson: (v) => (this.errorsJson = v),
|
||||||
|
setParsedErrorsJson: (v) => (this.parsed.errorsJson = v),
|
||||||
|
setParsedIncDeviceList: (v) => (this.parsed.incDeviceList = v),
|
||||||
|
onDevlis: async (json) => {
|
||||||
|
this.incDeviceList = json;
|
||||||
|
deviceConnection.handleDevListReceived(this.incDeviceList, this.deviceList, this.firstDevListRequest, {
|
||||||
|
setDeviceList: (list) => {
|
||||||
|
this.deviceList = list;
|
||||||
|
eventEmitter.emit("deviceListUpdated");
|
||||||
|
},
|
||||||
|
setFirstDevListRequest: (v) => (this.firstDevListRequest = v),
|
||||||
|
setParsedDeviceListJson: (v) => (this.parsed.deviceListJson = v),
|
||||||
|
onParced: () => this.onParced(),
|
||||||
|
selectedDeviceDataRefresh: () => this.selectedDeviceDataRefresh(),
|
||||||
|
connectToAllDevices: () => this.connectToAllDevices(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setFlashProfileJson: (v) => (this.flashProfileJson = v),
|
||||||
|
setParsedFlashProfileJson: (v) => (this.parsed.flashProfileJson = v),
|
||||||
|
setOtaJson: (v) => (this.otaJson = v),
|
||||||
|
setParsedOtaJson: (v) => (this.parsed.otaJson = v),
|
||||||
|
addCoreMsg: (msg) => this.addCoreMsg(msg),
|
||||||
|
onParced: () => this.onParced(),
|
||||||
|
};
|
||||||
|
|
||||||
|
this._allBlobHandlers = {
|
||||||
|
updateWidget: (v) => this.updateWidget(v),
|
||||||
|
combineLayoutsInOne: (ws, layout) => this.combineLayoutsInOne(ws, layout),
|
||||||
|
mergeParams: (devParams) => {
|
||||||
|
this.paramsJson = { ...this.paramsJson, ...devParams };
|
||||||
|
},
|
||||||
|
updateAllStatuses: (ws) => this.updateAllStatuses(ws),
|
||||||
|
onParced: () => this.onParced(),
|
||||||
|
apdateWidgetByArray: (v) => this.apdateWidgetByArray(v),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ack = wsReconnect.createAck({
|
||||||
|
markDeviceStatus: (ws, st) => this.markDeviceStatus(ws, st),
|
||||||
|
getDeviceList: () => this.deviceList,
|
||||||
|
setDeviceList: (list) => {
|
||||||
|
this.deviceList = list;
|
||||||
|
eventEmitter.emit("deviceListUpdated");
|
||||||
|
},
|
||||||
|
waitingAckTimeout: WAITING_ACK_TIMEOUT,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.wsTestMsgTask = wsReconnect.createWsTestMsgTask({
|
||||||
|
getDeviceList: () => this.deviceList,
|
||||||
|
send: (ws, msg) => this.wsSendMsg(ws, msg),
|
||||||
|
markDeviceStatus: (ws, st) => this.markDeviceStatus(ws, st),
|
||||||
|
connectDevice: (ws) => this._createConnection(ws, this.getIP(ws)),
|
||||||
|
ack: (ws, st) => this.ack(ws, st),
|
||||||
|
getRemainingTimeout: () => this.remainingTimeout,
|
||||||
|
setRemainingTimeout: (v) => (this.remainingTimeout = v),
|
||||||
|
reconnectTimeout: this.reconnectTimeout,
|
||||||
|
getPreventReconnect: () => this.preventReconnect,
|
||||||
|
setPercent: (v) => {
|
||||||
|
this.percent = v;
|
||||||
|
eventEmitter.emit("reconnectTick", {
|
||||||
|
percent: this.percent,
|
||||||
|
remainingTimeout: this.remainingTimeout,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getRebootOrUpdateProcess: () => this.rebootOrUpdateProcess,
|
||||||
|
setRebootOrUpdateProcess: (v) => (this.rebootOrUpdateProcess = v),
|
||||||
|
getSocketConnected: () => this.socketConnected,
|
||||||
|
setShowAwaitingCircle: (v) => (this.showAwaitingCircle = v),
|
||||||
|
setReconnectTimeout: (v) => (this.reconnectTimeout = v),
|
||||||
|
printAllCreatedWs: () => this._printAllCreatedWs(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getIP(ws) {
|
||||||
|
return deviceConnection.getIP(ws, this.deviceList);
|
||||||
|
}
|
||||||
|
|
||||||
|
wsSendMsg(ws, msg) {
|
||||||
|
if (deviceSocket.send(ws, msg)) {
|
||||||
|
if (this.debug) console.log("[i]", this.getIP(ws), ws, "msg send success", msg);
|
||||||
|
} else {
|
||||||
|
if (this.debug) console.log("[e]", this.getIP(ws), ws, "msg not send");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_createConnection(wsIndex, ip) {
|
||||||
|
if (ip === "error") {
|
||||||
|
if (this.debug) console.log("[e]", "device list wrong");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.debug) console.log("[i]", ip, wsIndex, "started connecting...");
|
||||||
|
deviceSocket.createConnection(wsIndex, ip, {
|
||||||
|
onOpen: (ws) => {
|
||||||
|
const fn = deviceConnection.createOpenHandler({
|
||||||
|
markDeviceStatus: (w, st) => this.markDeviceStatus(w, st),
|
||||||
|
sendMsg: (w, msg) => this.wsSendMsg(w, msg),
|
||||||
|
firstDevListRequest: this.firstDevListRequest,
|
||||||
|
currentPageName: this.currentPageName,
|
||||||
|
selectedWs: this.selectedWs,
|
||||||
|
sendCurrentPageNameToSelectedWs: () => this.sendCurrentPageNameToSelectedWs(),
|
||||||
|
});
|
||||||
|
fn(ws);
|
||||||
|
},
|
||||||
|
onMessage: (ws, data) => this._messageHandler(ws, data),
|
||||||
|
onClose: (ws) => this.markDeviceStatus(ws, false),
|
||||||
|
onError: (ws) => this.markDeviceStatus(ws, false),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_messageHandler(ws, data) {
|
||||||
|
if (typeof data === "string") {
|
||||||
|
if (data === "/tstr|") this.ack(ws, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data instanceof Blob) {
|
||||||
|
if (ws === this.selectedWs) this.parseBlob(data, ws);
|
||||||
|
if (this.currentPageName === "/|") this.parseAllBlob(data, ws);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectToAllDevices() {
|
||||||
|
deviceConnection.connectToAllDevices(
|
||||||
|
this.deviceList,
|
||||||
|
(ws) => this.getSelectedDeviceData(ws),
|
||||||
|
this.selectedWs,
|
||||||
|
(wsIndex, ip) => this._createConnection(wsIndex, ip)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_printAllCreatedWs() {
|
||||||
|
if (this.debug) console.log("[i]", "[ws]", "device count:", this.deviceList.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
markDeviceStatus(ws, status) {
|
||||||
|
this.deviceList.forEach((device) => {
|
||||||
|
if (device.ws === ws) {
|
||||||
|
device.status = status;
|
||||||
|
device.ping = 0;
|
||||||
|
if (device.status === true) {
|
||||||
|
console.log("[i]", device.ip, ws, "status online");
|
||||||
|
} else {
|
||||||
|
console.log("[i]", device.ip, ws, "status offline");
|
||||||
|
this.deleteWidget(ws);
|
||||||
|
this.sortingLayout(ws);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.selectedDeviceDataRefresh();
|
||||||
|
eventEmitter.emit("deviceListUpdated");
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteWidget(ws) {
|
||||||
|
this.layoutJson = this.layoutJson.filter((item) => item.ws !== ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseBlob(blob, ws) {
|
||||||
|
await blobProtocol.parseBlob(blob, ws, this._blobHandlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseAllBlob(blob, ws) {
|
||||||
|
await blobProtocol.parseAllBlob(blob, ws, this._allBlobHandlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onParced() {
|
||||||
|
if (this.currentPageName === "/|") {
|
||||||
|
this.pageReady.dash = true;
|
||||||
|
this._emitLayoutJsonUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.currentPageName === "/config|" &&
|
||||||
|
this.parsed.itemsJson &&
|
||||||
|
this.parsed.widgetsJson &&
|
||||||
|
this.parsed.configJson &&
|
||||||
|
this.parsed.settingsJson
|
||||||
|
) {
|
||||||
|
this.clearParcedFlags();
|
||||||
|
this.pageReady.config = true;
|
||||||
|
this._emitLayoutJsonUpdated();
|
||||||
|
eventEmitter.emit("configUpdated", {
|
||||||
|
configJson: this.configJson,
|
||||||
|
scenarioTxt: this.scenarioTxt,
|
||||||
|
widgetsJson: this.widgetsJson,
|
||||||
|
itemsJson: this.itemsJson,
|
||||||
|
});
|
||||||
|
if (this.debug) console.log("✔✔", "config page parced");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.currentPageName === "/connection|" &&
|
||||||
|
this.parsed.ssidJson &&
|
||||||
|
this.parsed.settingsJson &&
|
||||||
|
this.parsed.errorsJson
|
||||||
|
) {
|
||||||
|
this.clearParcedFlags();
|
||||||
|
if (this.debug) console.log("✔✔", "connection page parced");
|
||||||
|
this.pageReady.connection = true;
|
||||||
|
this._emitLayoutJsonUpdated();
|
||||||
|
eventEmitter.emit("connectionUpdated", {
|
||||||
|
settingsJson: this.settingsJson,
|
||||||
|
errorsJson: this.errorsJson,
|
||||||
|
ssidJson: this.ssidJson,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.currentPageName === "/list|" && this.parsed.settingsJson) {
|
||||||
|
this.clearParcedFlags();
|
||||||
|
if (this.debug) console.log("✔✔", "list page parced");
|
||||||
|
this.pageReady.list = true;
|
||||||
|
this._emitLayoutJsonUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.currentPageName === "/system|" && this.parsed.errorsJson && this.parsed.settingsJson) {
|
||||||
|
this.clearParcedFlags();
|
||||||
|
if (this.options.onSystemParsed) await this.options.onSystemParsed();
|
||||||
|
eventEmitter.emit("systemUpdated");
|
||||||
|
if (this.debug) console.log("✔✔", "system page parced");
|
||||||
|
this.pageReady.system = true;
|
||||||
|
this._emitLayoutJsonUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.currentPageName === "/profile|" && this.parsed.flashProfileJson) {
|
||||||
|
this.clearParcedFlags();
|
||||||
|
if (this.debug) console.log("✔✔", "profile page parced");
|
||||||
|
this.pageReady.profile = true;
|
||||||
|
if (this.options.onProfileParsed) await this.options.onProfileParsed();
|
||||||
|
this._emitLayoutJsonUpdated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_emitLayoutJsonUpdated() {
|
||||||
|
eventEmitter.emit("layoutJsonUpdated", {
|
||||||
|
layoutJson: this.layoutJson,
|
||||||
|
pages: this.pages,
|
||||||
|
pageReady: { ...this.pageReady },
|
||||||
|
configJson: this.configJson,
|
||||||
|
scenarioTxt: this.scenarioTxt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
devListOverride() {
|
||||||
|
this.deviceList = deviceListManager.devListOverride(this.incDeviceList);
|
||||||
|
console.log("[i]", "[devlist]", "devlist overrided");
|
||||||
|
eventEmitter.emit("deviceListUpdated");
|
||||||
|
}
|
||||||
|
|
||||||
|
devListCombine() {
|
||||||
|
this.deviceList = deviceListManager.devListCombine(this.deviceList, this.incDeviceList);
|
||||||
|
console.log("[i]", "[devlist]", "devlist combined");
|
||||||
|
eventEmitter.emit("deviceListUpdated");
|
||||||
|
}
|
||||||
|
|
||||||
|
combineArrays(A, B) {
|
||||||
|
return deviceListManager.combineArrays(A, B);
|
||||||
|
}
|
||||||
|
|
||||||
|
async combineLayoutsInOne(ws, devLayout) {
|
||||||
|
for (let i = 0; i < devLayout.length; i++) {
|
||||||
|
devLayout[i].ws = ws;
|
||||||
|
}
|
||||||
|
this.layoutJson = this.layoutJson.concat(devLayout);
|
||||||
|
console.log("[2]", ws, "devLayout pushed to layout");
|
||||||
|
this.sortingLayout(ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
sortingLayout(ws) {
|
||||||
|
this.layoutJson.sort((a, b) => (a.descr || "").localeCompare(b.descr || ""));
|
||||||
|
const pageSet = new Set(this.layoutJson.map((w) => w.page));
|
||||||
|
this.pages = [...pageSet].sort((a, b) => a.localeCompare(b)).map((page) => ({ page }));
|
||||||
|
if (this.debug) console.log("[3]", ws, "layout sort, requested params...");
|
||||||
|
this.wsSendMsg(ws, "/params|");
|
||||||
|
this._emitLayoutJsonUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAllStatuses(ws) {
|
||||||
|
const topicToIndex = new Map();
|
||||||
|
for (let i = 0; i < this.layoutJson.length; i++) {
|
||||||
|
const t = this.layoutJson[i].topic;
|
||||||
|
if (t) topicToIndex.set(t.slice(t.lastIndexOf("/") + 1), i);
|
||||||
|
}
|
||||||
|
for (const [key, value] of Object.entries(this.paramsJson)) {
|
||||||
|
const i = topicToIndex.get(key);
|
||||||
|
if (i !== undefined) {
|
||||||
|
if (this.debug) console.log("[i]", "updated =>" + key, value);
|
||||||
|
this.layoutJson[i].status = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.wsSendMsg(ws, "/charts|");
|
||||||
|
this._emitLayoutJsonUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWidget(newStatusJson) {
|
||||||
|
const i = this.layoutJson.findIndex((w) => w.topic === newStatusJson.topic);
|
||||||
|
if (i === -1) return;
|
||||||
|
this.jsonConcat(this.layoutJson[i], newStatusJson);
|
||||||
|
this.layoutJson[i].sent = false;
|
||||||
|
this._emitLayoutJsonUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
apdateWidgetByArray(newStatusJson) {
|
||||||
|
const i = this.layoutJson.findIndex((w) => w.topic === newStatusJson.topic);
|
||||||
|
if (i === -1) {
|
||||||
|
if (this.debug) console.log("[E]", "topic not found", newStatusJson.topic);
|
||||||
|
this._emitLayoutJsonUpdated();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.jsonConcatEx(this.layoutJson[i], newStatusJson);
|
||||||
|
const prev = this.layoutJson[i].status;
|
||||||
|
const next = newStatusJson.status;
|
||||||
|
this.layoutJson[i].status = Array.isArray(prev) ? [...prev, ...(next || [])] : next || [];
|
||||||
|
this.layoutJson[i].sent = false;
|
||||||
|
this._emitLayoutJsonUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonConcat(o1, o2) {
|
||||||
|
Object.assign(o1, o2);
|
||||||
|
return o1;
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonConcatEx(o1, o2) {
|
||||||
|
for (const key of Object.keys(o2)) {
|
||||||
|
if (key !== "status") o1[key] = o2[key];
|
||||||
|
}
|
||||||
|
return o1;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearData() {
|
||||||
|
this.itemsJson = [];
|
||||||
|
this.widgetsJson = [];
|
||||||
|
this.configJson = [];
|
||||||
|
this.scenarioTxt = " ";
|
||||||
|
this.settingsJson = {};
|
||||||
|
this.errorsJson = {};
|
||||||
|
this.layoutJson = [];
|
||||||
|
this.paramsJson = {};
|
||||||
|
this.otaJson = {};
|
||||||
|
this.flashProfileJson = {};
|
||||||
|
for (const key of Object.keys(this.pageReady)) {
|
||||||
|
this.pageReady[key] = false;
|
||||||
|
}
|
||||||
|
this.clearParcedFlags();
|
||||||
|
if (this.debug) console.log("[i]", "all json files cleared");
|
||||||
|
this._emitLayoutJsonUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearParcedFlags() {
|
||||||
|
console.log("[i]", "parced flags cleared");
|
||||||
|
for (const key of Object.keys(this.parsed)) {
|
||||||
|
this.parsed[key] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendCurrentPageNameToSelectedWs() {
|
||||||
|
if (this.selectedWs !== undefined) {
|
||||||
|
this.wsSendMsg(this.selectedWs, this.currentPageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendToAllDevices(msg) {
|
||||||
|
this.deviceList.forEach((device) => {
|
||||||
|
if (device.status === true) this.wsSendMsg(device.ws, msg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedDeviceData(ws) {
|
||||||
|
this.selectedDeviceData = this.deviceList.find((d) => d.ws === ws);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedDeviceDataRefresh() {
|
||||||
|
this.getSelectedDeviceData(this.selectedWs);
|
||||||
|
this.socketConnected = this.selectedDeviceData ? this.selectedDeviceData.status : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
wsPush(ws, topic, status) {
|
||||||
|
const key = topic.slice(topic.lastIndexOf("/") + 1);
|
||||||
|
this.wsSendMsg(ws, "/control|" + key + "/" + status);
|
||||||
|
}
|
||||||
|
|
||||||
|
addCoreMsg(msg) {
|
||||||
|
if (this.coreMessages.length >= LOG_MAX_MESSAGES) this.coreMessages.shift();
|
||||||
|
this.coreMessages.push({ msg, time: Date.now() });
|
||||||
|
this.coreMessages.sort((a, b) => (b.time || 0) - (a.time || 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
_getInput() {
|
||||||
|
return {
|
||||||
|
name: "inputDate",
|
||||||
|
widget: "input",
|
||||||
|
size: "small",
|
||||||
|
color: "orange",
|
||||||
|
type: "date",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_modify() {
|
||||||
|
for (let i = 0; i < this.configJson.length; i++) {
|
||||||
|
delete this.configJson[i]["show"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateLayout() {
|
||||||
|
const { mqttPrefix = "", id: settingsId = "" } = this.settingsJson;
|
||||||
|
const prefix = `${mqttPrefix}/${settingsId}/`;
|
||||||
|
const layout = [];
|
||||||
|
for (const config of this.configJson) {
|
||||||
|
const widget = this.widgetsJson.find((w) => w.name === config.widget);
|
||||||
|
if (!widget) {
|
||||||
|
if (this.debug) console.log("[E]", "widget not found:", config.widget);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (config.widget !== "nil") {
|
||||||
|
const item = { ...widget, page: config.page, descr: config.descr, topic: prefix + config.id };
|
||||||
|
layout.push(item);
|
||||||
|
if (item.widget === "chart" && item.type !== "bar") {
|
||||||
|
layout.push({
|
||||||
|
...this._getInput(),
|
||||||
|
page: config.page,
|
||||||
|
descr: config.descr,
|
||||||
|
topic: prefix + config.id + "-date",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layout.sort((a, b) => (a.descr || "").localeCompare(b.descr || ""));
|
||||||
|
layout.forEach((item, i) => (item.order = i));
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonArrWrite(jsonArr, idKey, idValue, paramKey, paramValue) {
|
||||||
|
for (let i = 0; i < jsonArr.length; i++) {
|
||||||
|
const obj = jsonArr[i];
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
if (key === idKey && value === idValue) {
|
||||||
|
obj[paramKey] = paramValue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveConfig() {
|
||||||
|
this.wsSendMsg(this.selectedWs, "/tuoyal|" + JSON.stringify(this.generateLayout()));
|
||||||
|
this._modify();
|
||||||
|
this.wsSendMsg(this.selectedWs, "/gifnoc|" + JSON.stringify(this.configJson));
|
||||||
|
this.wsSendMsg(this.selectedWs, "/oiranecs|" + this.scenarioTxt);
|
||||||
|
this.clearData();
|
||||||
|
this.sendCurrentPageNameToSelectedWs();
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSett() {
|
||||||
|
const size = Object.keys(this.settingsJson).length;
|
||||||
|
console.log("[i]", "settingsJson length: " + size);
|
||||||
|
if (size > 5) {
|
||||||
|
this.jsonArrWrite(
|
||||||
|
this.deviceList,
|
||||||
|
"ip",
|
||||||
|
this.getIP(this.selectedWs),
|
||||||
|
"name",
|
||||||
|
this.settingsJson.name
|
||||||
|
);
|
||||||
|
this.wsSendMsg(this.selectedWs, "/sgnittes|" + JSON.stringify(this.settingsJson));
|
||||||
|
eventEmitter.emit("deviceListUpdated");
|
||||||
|
} else {
|
||||||
|
window.alert("Ошибка размера settingsJson (возможно не был передан странице)");
|
||||||
|
}
|
||||||
|
this.clearData();
|
||||||
|
this.sendCurrentPageNameToSelectedWs();
|
||||||
|
}
|
||||||
|
|
||||||
|
saveList() {
|
||||||
|
const devListForSave = Object.assign([], this.deviceList);
|
||||||
|
for (let i = 0; i < devListForSave.length; i++) {
|
||||||
|
devListForSave[i].status = false;
|
||||||
|
}
|
||||||
|
this.wsSendMsg(this.selectedWs, "/tsil|" + JSON.stringify(devListForSave));
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanLogs() {
|
||||||
|
this.wsSendMsg(this.selectedWs, "/clean|");
|
||||||
|
}
|
||||||
|
|
||||||
|
saveMqtt() {
|
||||||
|
const size = Object.keys(this.settingsJson).length;
|
||||||
|
this.wsSendMsg(this.selectedWs, "/tuoyal|" + JSON.stringify(this.generateLayout()));
|
||||||
|
if (size > 5) {
|
||||||
|
this.wsSendMsg(this.selectedWs, "/sgnittes|" + JSON.stringify(this.settingsJson));
|
||||||
|
} else {
|
||||||
|
window.alert("Ошибка");
|
||||||
|
}
|
||||||
|
this.clearData();
|
||||||
|
this.wsSendMsg(this.selectedWs, "/mqtt|");
|
||||||
|
}
|
||||||
|
|
||||||
|
ssidClick() {
|
||||||
|
this.wsSendMsg(this.selectedWs, "/scan|");
|
||||||
|
}
|
||||||
|
|
||||||
|
rebootEsp() {
|
||||||
|
this.rebootOrUpdateProcess = true;
|
||||||
|
if (this.debug) console.log("[i]", "reboot...");
|
||||||
|
this.wsSendMsg(this.selectedWs, "/reboot|");
|
||||||
|
this.markDeviceStatus(this.selectedWs, false);
|
||||||
|
this.showAwaitingCircle = true;
|
||||||
|
this.socketConnected = false;
|
||||||
|
this.reconnectTimeout = 10;
|
||||||
|
this.remainingTimeout = this.reconnectTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBuild(path) {
|
||||||
|
this.rebootOrUpdateProcess = true;
|
||||||
|
console.log(path);
|
||||||
|
this.wsSendMsg(this.selectedWs, "/update|" + path);
|
||||||
|
this.showAwaitingCircle = true;
|
||||||
|
this.socketConnected = false;
|
||||||
|
this.reconnectTimeout = 20;
|
||||||
|
this.remainingTimeout = this.reconnectTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationReboot() {
|
||||||
|
console.log("[i]", "reboot svelte...");
|
||||||
|
for (const key of Object.keys(this.pageReady)) {
|
||||||
|
this.pageReady[key] = false;
|
||||||
|
}
|
||||||
|
this.showAwaitingCircle = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
location.reload();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelAlarm(alarmKey) {
|
||||||
|
console.log("[x]", alarmKey);
|
||||||
|
this.errorsJson[alarmKey] = 0;
|
||||||
|
this.wsSendMsg(this.selectedWs, '/rorre|{"' + alarmKey + '":0}');
|
||||||
|
}
|
||||||
|
|
||||||
|
moduleOrder(id, key, value) {
|
||||||
|
const json = { id, key, value };
|
||||||
|
this.wsSendMsg(this.selectedWs, "/order|" + JSON.stringify(json));
|
||||||
|
}
|
||||||
|
|
||||||
|
addDevInList() {
|
||||||
|
if (
|
||||||
|
this.newDevice.name !== undefined &&
|
||||||
|
this.newDevice.ip !== undefined &&
|
||||||
|
this.newDevice.id !== undefined
|
||||||
|
) {
|
||||||
|
this.newDevice.status = false;
|
||||||
|
this.newDevice.ws = this.deviceList.length;
|
||||||
|
this.incDeviceList.push(this.newDevice);
|
||||||
|
this.devListCombine();
|
||||||
|
this.connectToAllDevices();
|
||||||
|
if (this.debug) console.log("[i]", "selected device: ", this.selectedDeviceData);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.debug) console.log("[e]", "wrong data");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
startReconnectTask() {
|
||||||
|
this.wsTestMsgTask();
|
||||||
|
}
|
||||||
|
}
|
||||||
186
src/lib/blobProtocol.js
Normal file
186
src/lib/blobProtocol.js
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
/**
|
||||||
|
* Blob protocol: 6-byte header, 4-byte size (text), then payload.
|
||||||
|
* Helpers: getPayloadAsJson, getPayloadAsTxt, getJsonAsJson.
|
||||||
|
* parseBlob / parseAllBlob invoke callbacks for each header; no state.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse size string from blob (bytes 7-11) to number.
|
||||||
|
* @param {string} sizeStr
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
function parseSize(sizeStr) {
|
||||||
|
const n = parseInt(sizeStr, 10);
|
||||||
|
return isNaN(n) ? 0 : n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload as JSON (bytes size..length). Out: { json, parse }.
|
||||||
|
*/
|
||||||
|
export async function getPayloadAsJson(blob, size, out) {
|
||||||
|
const sizeNum = typeof size === "string" ? parseSize(size) : size;
|
||||||
|
const partBlob = blob.slice(sizeNum, blob.length);
|
||||||
|
const txt = await partBlob.text();
|
||||||
|
try {
|
||||||
|
out.json = JSON.parse(txt);
|
||||||
|
out.parse = true;
|
||||||
|
} catch (e) {
|
||||||
|
out.parse = false;
|
||||||
|
}
|
||||||
|
return out.parse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload as text (bytes size..length).
|
||||||
|
*/
|
||||||
|
export async function getPayloadAsTxt(blob, size) {
|
||||||
|
const sizeNum = typeof size === "string" ? parseSize(size) : size;
|
||||||
|
const txtBlob = blob.slice(sizeNum, blob.length);
|
||||||
|
return await txtBlob.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON from bytes 12..size (charta metadata only).
|
||||||
|
*/
|
||||||
|
export async function getJsonAsJson(blob, size, out) {
|
||||||
|
const sizeNum = typeof size === "string" ? parseSize(size) : size;
|
||||||
|
const partBlob = blob.slice(12, sizeNum);
|
||||||
|
const txt = await partBlob.text();
|
||||||
|
try {
|
||||||
|
out.json = JSON.parse(txt);
|
||||||
|
out.parse = true;
|
||||||
|
} catch (e) {
|
||||||
|
out.parse = false;
|
||||||
|
}
|
||||||
|
return out.parse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read header (6 bytes) and size (4 bytes) from blob.
|
||||||
|
*/
|
||||||
|
export async function readHeader(blob) {
|
||||||
|
const blobHeader = blob.slice(0, 6);
|
||||||
|
const header = await blobHeader.text();
|
||||||
|
const blobSize = blob.slice(7, 11);
|
||||||
|
const size = await blobSize.text();
|
||||||
|
return { header, size };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* parseBlob: selected device blobs. Calls handlers per header, then onParced.
|
||||||
|
* Handlers: setItemsJson, setWidgetsJson, setConfigJson, setScenarioTxt, setSettingsJson,
|
||||||
|
* setSsidJson, setErrorsJson, onDevlis(incDeviceList), setFlashProfileJson, setOtaJson, addCoreMsg, onParced.
|
||||||
|
* For each *Json also setParsed* (e.g. setParsedItemsJson(true)).
|
||||||
|
*/
|
||||||
|
export async function parseBlob(blob, ws, handlers) {
|
||||||
|
const { header, size } = await readHeader(blob);
|
||||||
|
const out = {};
|
||||||
|
|
||||||
|
if (header === "itemsj") {
|
||||||
|
if (await getPayloadAsJson(blob, size, out)) {
|
||||||
|
handlers.setItemsJson(out.json);
|
||||||
|
if (handlers.setParsedItemsJson) handlers.setParsedItemsJson(true);
|
||||||
|
} else if (handlers.setParsedItemsJson) handlers.setParsedItemsJson(false);
|
||||||
|
}
|
||||||
|
if (header === "widget") {
|
||||||
|
if (await getPayloadAsJson(blob, size, out)) {
|
||||||
|
handlers.setWidgetsJson(out.json);
|
||||||
|
if (handlers.setParsedWidgetsJson) handlers.setParsedWidgetsJson(true);
|
||||||
|
} else if (handlers.setParsedWidgetsJson) handlers.setParsedWidgetsJson(false);
|
||||||
|
}
|
||||||
|
if (header === "config") {
|
||||||
|
if (await getPayloadAsJson(blob, size, out)) {
|
||||||
|
handlers.setConfigJson(out.json);
|
||||||
|
if (handlers.setParsedConfigJson) handlers.setParsedConfigJson(true);
|
||||||
|
} else if (handlers.setParsedConfigJson) handlers.setParsedConfigJson(false);
|
||||||
|
}
|
||||||
|
if (header === "scenar") {
|
||||||
|
const txt = await getPayloadAsTxt(blob, size);
|
||||||
|
handlers.setScenarioTxt(txt);
|
||||||
|
}
|
||||||
|
if (header === "settin") {
|
||||||
|
if (await getPayloadAsJson(blob, size, out)) {
|
||||||
|
handlers.setSettingsJson(out.json);
|
||||||
|
if (handlers.setParsedSettingsJson) handlers.setParsedSettingsJson(true);
|
||||||
|
} else if (handlers.setParsedSettingsJson) handlers.setParsedSettingsJson(false);
|
||||||
|
}
|
||||||
|
if (header === "ssidli") {
|
||||||
|
if (await getPayloadAsJson(blob, size, out)) {
|
||||||
|
handlers.setSsidJson(out.json);
|
||||||
|
if (handlers.setParsedSsidJson) handlers.setParsedSsidJson(true);
|
||||||
|
} else if (handlers.setParsedSsidJson) handlers.setParsedSsidJson(false);
|
||||||
|
}
|
||||||
|
if (header === "errors") {
|
||||||
|
if (await getPayloadAsJson(blob, size, out)) {
|
||||||
|
handlers.setErrorsJson(out.json);
|
||||||
|
if (handlers.setParsedErrorsJson) handlers.setParsedErrorsJson(true);
|
||||||
|
} else if (handlers.setParsedErrorsJson) handlers.setParsedErrorsJson(false);
|
||||||
|
}
|
||||||
|
if (header === "devlis") {
|
||||||
|
if (await getPayloadAsJson(blob, size, out)) {
|
||||||
|
if (handlers.setParsedIncDeviceList) handlers.setParsedIncDeviceList(true);
|
||||||
|
if (handlers.onDevlis) await handlers.onDevlis(out.json);
|
||||||
|
} else if (handlers.setParsedIncDeviceList) handlers.setParsedIncDeviceList(false);
|
||||||
|
}
|
||||||
|
if (header === "prfile") {
|
||||||
|
if (await getPayloadAsJson(blob, size, out)) {
|
||||||
|
handlers.setFlashProfileJson(out.json);
|
||||||
|
if (handlers.setParsedFlashProfileJson) handlers.setParsedFlashProfileJson(true);
|
||||||
|
} else if (handlers.setParsedFlashProfileJson) handlers.setParsedFlashProfileJson(false);
|
||||||
|
}
|
||||||
|
if (header === "otaupd") {
|
||||||
|
if (await getPayloadAsJson(blob, size, out)) {
|
||||||
|
handlers.setOtaJson(out.json);
|
||||||
|
if (handlers.setParsedOtaJson) handlers.setParsedOtaJson(true);
|
||||||
|
} else if (handlers.setParsedOtaJson) handlers.setParsedOtaJson(false);
|
||||||
|
}
|
||||||
|
if (header === "corelg") {
|
||||||
|
const txt = await getPayloadAsTxt(blob, size);
|
||||||
|
if (handlers.addCoreMsg) handlers.addCoreMsg(txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handlers.onParced) await handlers.onParced();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* parseAllBlob: dashboard blobs (layout, status, params, charta, chartb).
|
||||||
|
* Handlers: updateWidget(statusJson), combineLayoutsInOne(ws, devLayout), mergeParams(devParams), updateAllStatuses(ws), onParced, apdateWidgetByArray(data).
|
||||||
|
*/
|
||||||
|
export async function parseAllBlob(blob, ws, handlers) {
|
||||||
|
const { header, size } = await readHeader(blob);
|
||||||
|
const out = {};
|
||||||
|
|
||||||
|
if (header === "status") {
|
||||||
|
if (await getPayloadAsJson(blob, size, out)) handlers.updateWidget(out.json);
|
||||||
|
}
|
||||||
|
if (header === "layout") {
|
||||||
|
if (await getPayloadAsJson(blob, size, out)) handlers.combineLayoutsInOne(ws, out.json);
|
||||||
|
}
|
||||||
|
if (header === "params") {
|
||||||
|
if (await getPayloadAsJson(blob, size, out)) {
|
||||||
|
handlers.mergeParams(out.json);
|
||||||
|
if (handlers.updateAllStatuses) handlers.updateAllStatuses(ws);
|
||||||
|
if (handlers.onParced) await handlers.onParced();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (header === "charta") {
|
||||||
|
// Two fragments: payload size..length is array text; metadata 12..size is JSON
|
||||||
|
const txt = await getPayloadAsTxt(blob, size);
|
||||||
|
const fixedTxt = "[" + txt.substring(0, txt.length - 1) + "]";
|
||||||
|
let chartJson;
|
||||||
|
try {
|
||||||
|
chartJson = JSON.parse(fixedTxt);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const addOut = {};
|
||||||
|
if (!(await getJsonAsJson(blob, size, addOut))) return;
|
||||||
|
const finalDataJson = { status: chartJson, ...addOut.json };
|
||||||
|
if (handlers.apdateWidgetByArray) handlers.apdateWidgetByArray(finalDataJson);
|
||||||
|
}
|
||||||
|
if (header === "chartb") {
|
||||||
|
if (await getPayloadAsJson(blob, size, out)) {
|
||||||
|
if (handlers.apdateWidgetByArray) handlers.apdateWidgetByArray(out.json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/lib/deviceConnection.js
Normal file
75
src/lib/deviceConnection.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* Device connection lifecycle: getIP, connectToAllDevices, onOpen behaviour, handleDevListReceived.
|
||||||
|
* No blob parsing; no socket implementation — receives createConnection from api.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { initDevList } from "./deviceListManager.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve device IP by ws index (device.ws === ws).
|
||||||
|
* @param {number} ws
|
||||||
|
* @param {Array} deviceList
|
||||||
|
* @returns {string} ip or "error"
|
||||||
|
*/
|
||||||
|
export function getIP(ws, deviceList) {
|
||||||
|
for (const device of deviceList) {
|
||||||
|
if (device.ws === ws) return device.ip;
|
||||||
|
}
|
||||||
|
return "error";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterate deviceList, assign ws = i; for each with status false/undefined call createConnection(i).
|
||||||
|
* @param {Array} deviceList - mutated: deviceList[i].ws = i
|
||||||
|
* @param {function} getSelectedDeviceData - (selectedWs) => void, called once at start
|
||||||
|
* @param {number} selectedWs
|
||||||
|
* @param {function} createConnection - (wsIndex, ip, callbacks) => void
|
||||||
|
*/
|
||||||
|
export function connectToAllDevices(deviceList, getSelectedDeviceData, selectedWs, createConnection) {
|
||||||
|
getSelectedDeviceData(selectedWs);
|
||||||
|
for (let i = 0; i < deviceList.length; i++) {
|
||||||
|
deviceList[i].ws = i;
|
||||||
|
if (deviceList[i].status === false || deviceList[i].status === undefined) {
|
||||||
|
const ip = getIP(i, deviceList);
|
||||||
|
if (ip !== "error") {
|
||||||
|
createConnection(i, ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build onOpen handler: mark online, send /devlist| when first and ws===0, send page name.
|
||||||
|
* @param {object} options - markDeviceStatus(ws, bool), sendMsg(ws, msg), firstDevListRequest, currentPageName, selectedWs, sendCurrentPageNameToSelectedWs()
|
||||||
|
* @returns {function(ws: number)} onOpen(ws)
|
||||||
|
*/
|
||||||
|
export function createOpenHandler(options) {
|
||||||
|
const {
|
||||||
|
markDeviceStatus,
|
||||||
|
sendMsg,
|
||||||
|
firstDevListRequest,
|
||||||
|
currentPageName,
|
||||||
|
selectedWs,
|
||||||
|
sendCurrentPageNameToSelectedWs,
|
||||||
|
} = options;
|
||||||
|
return function onOpen(ws) {
|
||||||
|
markDeviceStatus(ws, true);
|
||||||
|
if (firstDevListRequest && ws === 0) sendMsg(ws, "/devlist|");
|
||||||
|
if (currentPageName === "/|") {
|
||||||
|
sendMsg(ws, currentPageName);
|
||||||
|
} else {
|
||||||
|
if (ws === selectedWs) sendCurrentPageNameToSelectedWs();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When devlis blob received: run initDevList (override/combine, then connect).
|
||||||
|
* @param {Array} incDeviceList - list from device
|
||||||
|
* @param {Array} deviceList - current list (read)
|
||||||
|
* @param {boolean} firstDevListRequest
|
||||||
|
* @param {object} callbacks - setDeviceList, setFirstDevListRequest, setParsedDeviceListJson, onParced, selectedDeviceDataRefresh, connectToAllDevices
|
||||||
|
*/
|
||||||
|
export function handleDevListReceived(incDeviceList, deviceList, firstDevListRequest, callbacks) {
|
||||||
|
initDevList(deviceList, incDeviceList, firstDevListRequest, callbacks);
|
||||||
|
}
|
||||||
73
src/lib/deviceListManager.js
Normal file
73
src/lib/deviceListManager.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Device list data only: merge, sort, override/combine.
|
||||||
|
* No sockets or pages; state updates via callbacks.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge arrays by unique IP (from A, then B items whose ip not in A).
|
||||||
|
* @param {Array} A
|
||||||
|
* @param {Array} B
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
export function combineArrays(A, B) {
|
||||||
|
const ids = new Set(A.map((d) => d.ip));
|
||||||
|
return [...A, ...B.filter((d) => !ids.has(d.ip))];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort list by name, keep first element at index 0.
|
||||||
|
* Mutates list in place.
|
||||||
|
* @param {Array} list
|
||||||
|
*/
|
||||||
|
export function sortList(list) {
|
||||||
|
const firstDev = list.shift();
|
||||||
|
list.sort((a, b) => {
|
||||||
|
if (a.name < b.name) return -1;
|
||||||
|
if (a.name > b.name) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
list.unshift(firstDev);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace deviceList with incDeviceList, sort, set first device status true.
|
||||||
|
* @param {Array} incDeviceList
|
||||||
|
* @returns {Array} new device list
|
||||||
|
*/
|
||||||
|
export function devListOverride(incDeviceList) {
|
||||||
|
const list = [...incDeviceList];
|
||||||
|
sortList(list);
|
||||||
|
list[0].status = true;
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge deviceList with incDeviceList by IP, sort.
|
||||||
|
* @param {Array} deviceList
|
||||||
|
* @param {Array} incDeviceList
|
||||||
|
* @returns {Array} new device list
|
||||||
|
*/
|
||||||
|
export function devListCombine(deviceList, incDeviceList) {
|
||||||
|
const list = combineArrays(deviceList, incDeviceList);
|
||||||
|
sortList(list);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init after receiving devlis: override or combine, then run callbacks.
|
||||||
|
* @param {Array} deviceList
|
||||||
|
* @param {Array} incDeviceList
|
||||||
|
* @param {boolean} firstDevListRequest
|
||||||
|
* @param {object} callbacks - setDeviceList(list), setFirstDevListRequest(bool), connectToAllDevices(), onParced(), selectedDeviceDataRefresh(), setParsedDeviceListJson(bool)
|
||||||
|
*/
|
||||||
|
export function initDevList(deviceList, incDeviceList, firstDevListRequest, callbacks) {
|
||||||
|
const list = firstDevListRequest
|
||||||
|
? devListOverride(incDeviceList)
|
||||||
|
: devListCombine(deviceList, incDeviceList);
|
||||||
|
callbacks.setDeviceList(list);
|
||||||
|
callbacks.setFirstDevListRequest(false);
|
||||||
|
if (callbacks.setParsedDeviceListJson) callbacks.setParsedDeviceListJson(true);
|
||||||
|
if (callbacks.onParced) callbacks.onParced();
|
||||||
|
if (callbacks.selectedDeviceDataRefresh) callbacks.selectedDeviceDataRefresh();
|
||||||
|
if (callbacks.connectToAllDevices) callbacks.connectToAllDevices();
|
||||||
|
}
|
||||||
95
src/lib/wsReconnect.js
Normal file
95
src/lib/wsReconnect.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
/**
|
||||||
|
* Reconnect ticker and ack timeouts (heartbeat). No deviceConnection import; getIP/connectDevice passed in.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scale number from one range to another.
|
||||||
|
*/
|
||||||
|
function scale(number, inMin, inMax, outMin, outMax) {
|
||||||
|
return ((number - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ackTimeoutsStore = {};
|
||||||
|
const startMillisStore = {};
|
||||||
|
const pingStore = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create ack(ws, st). On st false: set timeout to call markDeviceStatus(ws, false). On st true: clear timeout, set ping on deviceList.
|
||||||
|
* @param {object} deps - markDeviceStatus(ws, bool), getDeviceList(), setDeviceList(list), waitingAckTimeout
|
||||||
|
* @returns {function(ws: number, st: boolean)} ack
|
||||||
|
*/
|
||||||
|
export function createAck(deps) {
|
||||||
|
const { markDeviceStatus, getDeviceList, setDeviceList, waitingAckTimeout } = deps;
|
||||||
|
return function ack(ws, st) {
|
||||||
|
if (!st) {
|
||||||
|
startMillisStore[ws] = Date.now();
|
||||||
|
ackTimeoutsStore[ws] = setTimeout(() => {
|
||||||
|
markDeviceStatus(ws, false);
|
||||||
|
}, waitingAckTimeout);
|
||||||
|
} else {
|
||||||
|
if (ackTimeoutsStore[ws]) clearTimeout(ackTimeoutsStore[ws]);
|
||||||
|
if (startMillisStore[ws]) pingStore[ws] = Date.now() - startMillisStore[ws];
|
||||||
|
const deviceList = getDeviceList();
|
||||||
|
for (let i = 0; i < deviceList.length; i++) {
|
||||||
|
if (deviceList[i].ws === ws) deviceList[i].ping = pingStore[ws];
|
||||||
|
}
|
||||||
|
setDeviceList(deviceList);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the 1s ticker: decrement remainingTimeout, update percent; at 0 reconnect or send /tst| and ack.
|
||||||
|
* @param {object} deps - getDeviceList, send(ws, msg), markDeviceStatus, connectDevice(ws), ack(ws, st), getRemainingTimeout, setRemainingTimeout, reconnectTimeout, getPreventReconnect, setPercent, getRebootOrUpdateProcess, getSocketConnected, setShowAwaitingCircle, setReconnectTimeout, printAllCreatedWs (optional)
|
||||||
|
* @returns {function()} wsTestMsgTask - call once to start; it reschedules itself via setTimeout(wsTestMsgTask, 1000)
|
||||||
|
*/
|
||||||
|
export function createWsTestMsgTask(deps) {
|
||||||
|
const {
|
||||||
|
getDeviceList,
|
||||||
|
send,
|
||||||
|
markDeviceStatus,
|
||||||
|
connectDevice,
|
||||||
|
ack,
|
||||||
|
getRemainingTimeout,
|
||||||
|
setRemainingTimeout,
|
||||||
|
reconnectTimeout,
|
||||||
|
getPreventReconnect,
|
||||||
|
setPercent,
|
||||||
|
getRebootOrUpdateProcess,
|
||||||
|
setRebootOrUpdateProcess,
|
||||||
|
getSocketConnected,
|
||||||
|
setShowAwaitingCircle,
|
||||||
|
setReconnectTimeout,
|
||||||
|
printAllCreatedWs,
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
function wsTestMsgTask() {
|
||||||
|
const schedule = () => setTimeout(wsTestMsgTask, 1000);
|
||||||
|
schedule(); // reschedule first (same as original)
|
||||||
|
if (getPreventReconnect()) return;
|
||||||
|
let remaining = getRemainingTimeout() - 1;
|
||||||
|
if (getRebootOrUpdateProcess() && getSocketConnected()) {
|
||||||
|
if (setRebootOrUpdateProcess) setRebootOrUpdateProcess(false);
|
||||||
|
if (setReconnectTimeout) setReconnectTimeout(60);
|
||||||
|
remaining = 60;
|
||||||
|
if (setShowAwaitingCircle) setShowAwaitingCircle(false);
|
||||||
|
}
|
||||||
|
setRemainingTimeout(remaining);
|
||||||
|
setPercent(scale(remaining, reconnectTimeout, 0, 0, 100));
|
||||||
|
if (remaining <= 0) {
|
||||||
|
if (setReconnectTimeout) setReconnectTimeout(reconnectTimeout);
|
||||||
|
setRemainingTimeout(reconnectTimeout);
|
||||||
|
if (printAllCreatedWs) printAllCreatedWs();
|
||||||
|
const deviceList = getDeviceList();
|
||||||
|
deviceList.forEach((device) => {
|
||||||
|
if (device.status === false || device.status === undefined) {
|
||||||
|
connectDevice(device.ws);
|
||||||
|
} else {
|
||||||
|
send(device.ws, "/tst|");
|
||||||
|
ack(device.ws, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wsTestMsgTask;
|
||||||
|
}
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
import OpenIcon from "../svg/Open.svelte";
|
import OpenIcon from "../svg/Open.svelte";
|
||||||
import Alarm from "../components/Alarm.svelte";
|
import Alarm from "../components/Alarm.svelte";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
import * as portal from "../api/portal.js";
|
||||||
|
|
||||||
export let configJson;
|
export let configJson;
|
||||||
export let widgetsJson;
|
export let widgetsJson;
|
||||||
@@ -206,16 +206,10 @@
|
|||||||
export let moduleOrder = (id, key, value) => {};
|
export let moduleOrder = (id, key, value) => {};
|
||||||
|
|
||||||
const makePost = async () => {
|
const makePost = async () => {
|
||||||
let iotmpostDefault = {
|
const body = {
|
||||||
category: "",
|
category: "",
|
||||||
topic: {
|
topic: { ru: "", en: "" },
|
||||||
ru: "",
|
text: { ru: "", en: "" },
|
||||||
en: "",
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
ru: "",
|
|
||||||
en: "",
|
|
||||||
},
|
|
||||||
config: configJson,
|
config: configJson,
|
||||||
scenario: scenarioTxt,
|
scenario: scenarioTxt,
|
||||||
gallery: [],
|
gallery: [],
|
||||||
@@ -223,51 +217,25 @@
|
|||||||
username: userdata.username,
|
username: userdata.username,
|
||||||
};
|
};
|
||||||
const JWT = Cookies.get("token_iotm2");
|
const JWT = Cookies.get("token_iotm2");
|
||||||
try {
|
const res = await portal.addConfiguration(JWT, body);
|
||||||
let res = await fetch("https://portal.iotmanager.org/api/configurations/add", {
|
if (res.ok && res.data && res.data.result) {
|
||||||
mode: "cors",
|
console.log(res.data.result);
|
||||||
method: "POST",
|
if (res.data.result.acknowledged) {
|
||||||
headers: {
|
window.open(portal.BASE + "/configs?id=" + res.data.result.insertedId + "&token=" + JWT, "_blank");
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${JWT}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(iotmpostDefault),
|
|
||||||
});
|
|
||||||
const content = await res.json();
|
|
||||||
if (res.ok) {
|
|
||||||
console.log(content.result);
|
|
||||||
if (content.result.acknowledged) {
|
|
||||||
window.open("https://portal.iotmanager.org/configs?id=" + content.result.insertedId + "&token=" + Cookies.get("token_iotm2"), "_blank");
|
|
||||||
}
|
}
|
||||||
errors = [{ msg: "ok_success" }];
|
errors = [{ msg: "ok_success" }];
|
||||||
} else {
|
} else if (res.errors) {
|
||||||
errors = content.message;
|
errors = res.errors;
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getConfigs = async () => {
|
const getConfigs = async () => {
|
||||||
try {
|
|
||||||
const JWT = Cookies.get("token_iotm2");
|
const JWT = Cookies.get("token_iotm2");
|
||||||
let res = await fetch("https://portal.iotmanager.org/api/configurations/get", {
|
const res = await portal.getConfigurations(JWT);
|
||||||
headers: {
|
if (res.ok && res.configurations != null) {
|
||||||
"Content-Type": "application/json",
|
configurations = res.configurations;
|
||||||
Authorization: `Bearer ${JWT}`,
|
|
||||||
},
|
|
||||||
mode: "cors",
|
|
||||||
method: "GET",
|
|
||||||
});
|
|
||||||
if (res.ok) {
|
|
||||||
configurations = await res.json();
|
|
||||||
//configurations.push({ topic: { ru: "123" } });
|
|
||||||
console.log("error", configurations);
|
|
||||||
} else {
|
} else {
|
||||||
console.log("error", res.statusText);
|
console.log("error", "getConfigurations");
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log("error", e);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
<Range bind:value={widget.status} widget={widget} wsPush={(ws, topic, status) => wsPush(ws, topic, status)} />
|
<Range bind:value={widget.status} widget={widget} wsPush={(ws, topic, status) => wsPush(ws, topic, status)} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if widget.widget === "chart"}
|
{#if widget.widget === "chart"}
|
||||||
<Chart bind:value={widget.status} widget={widget} />
|
<Chart widget={widget} />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
<script>
|
|
||||||
import Card from "../components/Card.svelte";
|
|
||||||
|
|
||||||
export let coreMessages;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Card title={"Лог"}>
|
|
||||||
{#each coreMessages as message, i}
|
|
||||||
<div class={message.msg.toString().includes("[E]") ? "text-red-500" : "text-black"}>{message.msg}</div>
|
|
||||||
{/each}
|
|
||||||
</Card>
|
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
import { t, locale, locales } from "../i18n";
|
import { t, locale, locales } from "../i18n";
|
||||||
import { router } from "tinro";
|
import { router } from "tinro";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
import * as portal from "../api/portal.js";
|
||||||
export let show = true;
|
export let show = true;
|
||||||
export let serverOnline;
|
export let serverOnline;
|
||||||
|
|
||||||
@@ -12,25 +13,12 @@
|
|||||||
|
|
||||||
const login = async (user) => {
|
const login = async (user) => {
|
||||||
errors = [];
|
errors = [];
|
||||||
try {
|
const res = await portal.login(user);
|
||||||
let res = await fetch("https://portal.iotmanager.org/api/auth/login", {
|
if (res.ok && res.data && res.data.message) {
|
||||||
mode: "cors",
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Accept: "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(user),
|
|
||||||
});
|
|
||||||
const content = await res.json();
|
|
||||||
if (res.ok) {
|
|
||||||
errors = [{ msg: "ok_success_login" }];
|
errors = [{ msg: "ok_success_login" }];
|
||||||
saveToken(content.message);
|
saveToken(res.data.message);
|
||||||
} else {
|
} else if (res.data && res.data.message) {
|
||||||
errors = content.message;
|
errors = res.data.message;
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { router } from "tinro";
|
import { router } from "tinro";
|
||||||
import { t, locale, locales } from "../i18n";
|
import { t, locale, locales } from "../i18n";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
import * as portal from "../api/portal.js";
|
||||||
export let show;
|
export let show;
|
||||||
export let flashProfileJson;
|
export let flashProfileJson;
|
||||||
export let otaJson;
|
export let otaJson;
|
||||||
@@ -51,73 +52,33 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getUserBuilds = async () => {
|
const getUserBuilds = async () => {
|
||||||
try {
|
|
||||||
const JWT = Cookies.get("token_iotm2");
|
const JWT = Cookies.get("token_iotm2");
|
||||||
let res = await fetch("https://portal.iotmanager.org/compiler/userorders", {
|
const res = await portal.getUserOrders(JWT);
|
||||||
headers: {
|
if (res.ok && res.userBuilds != null) {
|
||||||
"Content-Type": "application/json",
|
userBuilds = res.userBuilds;
|
||||||
Authorization: `Bearer ${JWT}`,
|
|
||||||
},
|
|
||||||
mode: "cors",
|
|
||||||
method: "GET",
|
|
||||||
});
|
|
||||||
if (res.ok) {
|
|
||||||
userBuilds = await res.json();
|
|
||||||
checkStatus(userBuilds);
|
checkStatus(userBuilds);
|
||||||
} else {
|
} else {
|
||||||
console.log("error", res.statusText);
|
console.log("error", "getUserOrders");
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log("error", e);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const delBuild = async (ord) => {
|
const delBuild = async (ord) => {
|
||||||
try {
|
|
||||||
const JWT = Cookies.get("token_iotm2");
|
const JWT = Cookies.get("token_iotm2");
|
||||||
let res = await fetch("https://portal.iotmanager.org/compiler/delete/builds/" + ord.orderId, {
|
const res = await portal.deleteBuild(JWT, ord.orderId);
|
||||||
headers: {
|
if (res.ok) await getUserBuilds();
|
||||||
"Content-Type": "application/json",
|
else console.log("error", "deleteBuild");
|
||||||
Authorization: `Bearer ${JWT}`,
|
|
||||||
},
|
|
||||||
mode: "cors",
|
|
||||||
method: "GET",
|
|
||||||
});
|
|
||||||
if (res.ok) {
|
|
||||||
await getUserBuilds();
|
|
||||||
} else {
|
|
||||||
console.log("error", res.statusText);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log("error", e);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const placeOrder = async () => {
|
const placeOrder = async () => {
|
||||||
delete profile["_id"];
|
delete profile["_id"];
|
||||||
//добавим в тело имя пользователя
|
|
||||||
profile.username = userdata.username;
|
profile.username = userdata.username;
|
||||||
const JWT = Cookies.get("token_iotm2");
|
const JWT = Cookies.get("token_iotm2");
|
||||||
try {
|
const res = await portal.placeOrder(JWT, profile);
|
||||||
let res = await fetch("https://portal.iotmanager.org/compiler/order", {
|
|
||||||
mode: "cors",
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${JWT}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(profile),
|
|
||||||
});
|
|
||||||
const content = await res.json();
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
errors = [{ msg: "ok_success" }];
|
errors = [{ msg: "ok_success" }];
|
||||||
await getUserBuilds();
|
await getUserBuilds();
|
||||||
console.log(content.message);
|
} else if (res.errors) {
|
||||||
} else {
|
errors = res.errors;
|
||||||
errors = content.message;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -236,7 +197,7 @@
|
|||||||
</td>
|
</td>
|
||||||
{#if build.status.build === 2 && build.status.preparation === 2 && build.status.fs === 2}
|
{#if build.status.build === 2 && build.status.preparation === 2 && build.status.fs === 2}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<td on:click={() => updateBuild("http://portal.iotmanager.org/compiler/userdata/builds/" + build.orderId)} class="tbl-bdy-lg ipt-lg w-full cursor-pointer select-none bg-green-100 hover:bg-green-200">
|
<td on:click={() => updateBuild(portal.BASE + "/compiler/userdata/builds/" + build.orderId)} class="tbl-bdy-lg ipt-lg w-full cursor-pointer select-none bg-green-100 hover:bg-green-200">
|
||||||
<p class="w-fill">Установить</p>
|
<p class="w-fill">Установить</p>
|
||||||
</td>
|
</td>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -131,8 +131,8 @@
|
|||||||
|
|
||||||
export let errorsJson;
|
export let errorsJson;
|
||||||
|
|
||||||
//export let versionsList;
|
export let versionsList = {};
|
||||||
//export let choosingVersion;
|
export let choosingVersion;
|
||||||
export let coreMessages;
|
export let coreMessages;
|
||||||
export let settingsJson;
|
export let settingsJson;
|
||||||
|
|
||||||
@@ -147,7 +147,10 @@
|
|||||||
|
|
||||||
let reboot = false;
|
let reboot = false;
|
||||||
|
|
||||||
//export let cancelAlarm = (alarmKey) => {};
|
export let cancelAlarm = (alarmKey) => {};
|
||||||
|
|
||||||
|
// Referenced by parent (App); used in commented-out template blocks (versions dropdown, alarm close)
|
||||||
|
$: __propsRef = { versionsList, choosingVersion, cancelAlarm };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if show}
|
{#if show}
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
<script>
|
|
||||||
import Card from "../components/Card.svelte";
|
|
||||||
import Alarm from "../components/Alarm.svelte";
|
|
||||||
import Chart from "svelte-frappe-charts";
|
|
||||||
|
|
||||||
let datachart = {
|
|
||||||
labels: ["Sun", "Mon", "Tues", "Wed", "Thurs", "Fri", "Sat"],
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
values: [10, 12, 3, 9, 8, 15, 9],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
//объединение двух массивов с удалением дубликатов полностью
|
|
||||||
let a = [
|
|
||||||
{ item: "1", description: "lorem" },
|
|
||||||
{ item: "2", description: "impsum" },
|
|
||||||
];
|
|
||||||
let b = [
|
|
||||||
{ item: "2", description: "dolor" },
|
|
||||||
{ item: "4", description: "enum" },
|
|
||||||
];
|
|
||||||
function joinWithoutDupes(A, B) {
|
|
||||||
let output = [];
|
|
||||||
const a = new Set(A.map((x) => x.item));
|
|
||||||
const b = new Set(B.map((x) => x.item));
|
|
||||||
output = [...A.filter((x) => !b.has(x.item)), ...B.filter((x) => !a.has(x.item))];
|
|
||||||
console.log(output);
|
|
||||||
}
|
|
||||||
//объединение двух массивов с удалением дубликатов, оставляя один из дубликатов
|
|
||||||
let c = [
|
|
||||||
{ ID: "1", description: "lorem" },
|
|
||||||
{ ID: "2", description: "impsum" },
|
|
||||||
];
|
|
||||||
let d = [
|
|
||||||
{ ID: "2", description: "dolor" },
|
|
||||||
{ ID: "4", description: "enum" },
|
|
||||||
];
|
|
||||||
function joinWithoutDupesAndRmooving(A, B) {
|
|
||||||
var ids = new Set(A.map((d) => d.ID));
|
|
||||||
let output = [...A, ...B.filter((d) => !ids.has(d.ID))];
|
|
||||||
console.log(output);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Alarm>
|
|
||||||
<button class="btn-lg" on:click={() => joinWithoutDupesAndRmooving(c, d)}>{"Проверить"}</button>
|
|
||||||
</Alarm>
|
|
||||||
|
|
||||||
<Alarm>
|
|
||||||
<Chart data={datachart} type="line" />
|
|
||||||
</Alarm>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script>
|
|
||||||
export let color;
|
|
||||||
export let x = 0;
|
|
||||||
export let y = 0;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svg class="h-8 w-8 {color}" width="8" height="8" viewBox="{x} {y} 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" /> <rect x="4" y="4" width="16" height="16" rx="2" /> <line x1="4" y1="12" x2="20" y2="12" /> <line x1="12" y1="4" x2="12" y2="20" /></svg>
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<script>
|
|
||||||
let dataReceived;
|
|
||||||
let awaiting = 0;
|
|
||||||
|
|
||||||
async function awaitingData() {
|
|
||||||
console.log("Awaiting Data start...");
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
setTimeout(() => reject(), 10000);
|
|
||||||
dataReceived = resolve;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function startAwaiting() {
|
|
||||||
awaiting = awaitingData();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onReceive() {
|
|
||||||
dataReceived();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#await awaiting}
|
|
||||||
<p>Connecting...</p>
|
|
||||||
<button on:click={() => onReceive()}>Resolve</button>
|
|
||||||
{:then}
|
|
||||||
<button on:click={() => startAwaiting()}>Start</button>
|
|
||||||
{:catch}
|
|
||||||
<p>error</p>
|
|
||||||
{/await}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script>
|
|
||||||
// вывести время чего либо в консоль
|
|
||||||
var t0 = performance.now();
|
|
||||||
var t1 = performance.now();
|
|
||||||
console.log("layout time: " + (t1 - t0) + " mls");
|
|
||||||
//
|
|
||||||
</script>
|
|
||||||
@@ -3,10 +3,10 @@
|
|||||||
export let widget;
|
export let widget;
|
||||||
|
|
||||||
let datachart = {
|
let datachart = {
|
||||||
labels: [0, 0],
|
labels: ["0", "0"],
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
name: widget.descr,
|
name: (widget && widget.descr) ? widget.descr : "",
|
||||||
values: [0, 0],
|
values: [0, 0],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -36,33 +36,40 @@
|
|||||||
|
|
||||||
function collectDataToArr() {
|
function collectDataToArr() {
|
||||||
if (prevStatus !== widget.status && !firstTime) {
|
if (prevStatus !== widget.status && !firstTime) {
|
||||||
if (Array.isArray(widget.status)) {
|
if (Array.isArray(widget.status) && widget.status.length > 0) {
|
||||||
//console.log("[i]", "=======================================================");
|
|
||||||
prevStatus = widget.status;
|
prevStatus = widget.status;
|
||||||
|
|
||||||
if (widget.maxCount === 0) {
|
if (widget.maxCount === 0) {
|
||||||
clearCart();
|
clearCart();
|
||||||
widget.status = [];
|
widget.status = [];
|
||||||
console.log("[i]", "clear cart data");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const safeLabels = [];
|
||||||
|
const safeValues = [];
|
||||||
for (let i = 0; i < widget.status.length; i++) {
|
for (let i = 0; i < widget.status.length; i++) {
|
||||||
|
const pt = widget.status[i];
|
||||||
|
const x = Number(pt?.x);
|
||||||
|
const y1 = Number(pt?.y1);
|
||||||
|
if (Number.isNaN(x) || Number.isNaN(y1)) continue;
|
||||||
if (type === "bar") {
|
if (type === "bar") {
|
||||||
labels[i] = getDDMM(widget.status[i].x);
|
safeLabels.push(getDDMM(x));
|
||||||
} else if (i === 0) {
|
} else if (i === 0) {
|
||||||
labels[i] = getDDMM(widget.status[i].x);
|
safeLabels.push(getDDMM(x));
|
||||||
} else {
|
} else {
|
||||||
labels[i] = getHHMM(widget.status[i].x);
|
safeLabels.push(getHHMM(x));
|
||||||
}
|
}
|
||||||
values[i] = [widget.status[i].y1];
|
safeValues.push([y1]);
|
||||||
}
|
}
|
||||||
|
if (safeLabels.length === 0 || safeValues.length === 0) return;
|
||||||
|
|
||||||
|
labels = safeLabels;
|
||||||
|
values = safeValues;
|
||||||
datachart = {
|
datachart = {
|
||||||
labels: labels,
|
labels: labels,
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
name: widget.descr,
|
name: widget.descr || "",
|
||||||
values: values,
|
values: values,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -86,18 +93,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearCart() {
|
function clearCart() {
|
||||||
widget.status = [];
|
if (widget) widget.status = [];
|
||||||
|
|
||||||
labels = [];
|
labels = [];
|
||||||
values = [];
|
values = [];
|
||||||
|
|
||||||
datachart = {
|
datachart = {
|
||||||
labels: [0, 0],
|
labels: ["0", "0"],
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{ name: (widget && widget.descr) ? widget.descr : "", values: [0, 0] },
|
||||||
name: widget.descr,
|
|
||||||
values: [0, 0],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user