Skip to content

Denisov-Foundation-Limited/plc-cloud

Repository files navigation

plc-cloud

🏠 ☁️ ⚙️ 🤖

Облачный сервер для PLC/ESP32 с веб-интерфейсом и Telegram-ботом

Node.js Express WebSocket Telegram

plc-cloud - облачный сервер для PLC/ESP32-контроллеров с веб-интерфейсом, браузерным WebSocket API и Telegram-ботом.

Назначение

🌐 Один сервис для веба, устройств и Telegram-управления с единым runtime state.

Проект решает несколько задач:

  • аутентификация пользователей веб-интерфейса;
  • хранение объектов, устройств, пользователей и Telegram-настроек в SQLite;
  • поддержание онлайн-сессий устройств;
  • проксирование протокольных get/cmd команд между браузером, Telegram и PLC;
  • фильтрация объектов, устройств, контроллеров и команд через ACL, приходящий от PLC;
  • отправка Telegram-уведомлений об онлайне, офлайне и событиях устройств.

Текущая схема запуска

Контур Адрес Назначение
Web UI / REST http://HOST:WEB_PORT фронтенд, REST API
Browser WS ws://HOST:WEB_PORT/ws/web браузерные команды и подписки
Device WS ws://HOST:PORT/ws/device подключение PLC/ESP32
  • Web UI и REST API: http://HOST:WEB_PORT
  • Browser WS: ws://HOST:WEB_PORT/ws/web
  • Device WS: ws://HOST:PORT/ws/device

Порты по умолчанию:

  • WEB_PORT=80
  • PORT=3001

Приложение поднимает два отдельных listener:

  • web listener для статики, REST и /ws/web
  • device listener для /ws/device

Стек

⚙️ Express   ⚡ ws   🧩 awilix   🤖 grammY   💾 SQLite
  • Express
  • ws
  • awilix
  • grammY
  • sequelize + sqlite3
  • in-memory runtime state в DeviceRegistry

Общая архитектура

flowchart LR
    subgraph Browser["Браузер"]
        UI["SPA интерфейс"]
        BWS["WS /ws/web"]
    end

    subgraph Telegram["Telegram"]
        TG["Бот grammY<br/>long polling"]
    end

    subgraph Cloud["plc-cloud"]
        HTTP["Express + REST + static"]
        WSS["WebWsServer"]
        DWS["DeviceWsServer"]
        ACL["AccessControl"]
        REG["DeviceRegistry"]
        SES["SessionStore"]
        UDB["UsersDb"]
        DDB["DevicesDb"]
        TDB["TelegramConfigDb"]
        BOT["TelegramBotService"]
    end

    subgraph PLC["PLC / ESP32"]
        DEV["Контроллер"]
    end

    UI --> HTTP
    UI --> BWS
    BWS --> WSS
    TG --> BOT
    BOT --> DWS
    HTTP --> ACL
    WSS --> ACL
    ACL --> REG
    HTTP --> UDB
    HTTP --> DDB
    HTTP --> TDB
    DEV --> DWS
    DWS --> REG
    DWS --> DDB
    BOT --> REG
Loading

Поток данных от устройства

sequenceDiagram
    participant PLC as PLC/ESP32
    participant WS as DeviceWsServer
    participant DB as DevicesDb
    participant REG as DeviceRegistry
    participant WEB as WebWsServer
    participant TG as TelegramBotService

    PLC->>WS: hello + auth.api_key
    WS->>DB: getByApiKey(api_key)
    DB-->>WS: устройство найдено
    WS->>REG: attach(session)
    WS-->>PLC: hello_ack(session_id)
    WS-->>WEB: device_online
    WS-->>TG: notifyDeviceOnline()

    PLC->>WS: result / ack / event
    WS->>REG: merge runtime state
    WS->>DB: updateLastSeen()
    WS-->>WEB: device_update
    WS-->>TG: notifyDeviceUpdate()
Loading

Поток команды из браузера

sequenceDiagram
    participant UI as Браузер
    participant WEB as WebWsServer
    participant ACL as AccessControl
    participant DWS as DeviceWsServer
    participant PLC as PLC/ESP32

    UI->>WEB: send_get / send_cmd
    WEB->>ACL: проверка доступа
    ACL-->>WEB: ok / deny
    WEB->>DWS: sendGet() / sendCmd()
    DWS->>PLC: get / cmd
    PLC-->>DWS: result / ack
    DWS-->>WEB: device_update
Loading

Поток команды из Telegram

sequenceDiagram
    participant User as Пользователь
    participant TG as TelegramBotService
    participant UDB as UsersDb
    participant ACL as AccessControl
    participant DWS as DeviceWsServer
    participant PLC as PLC/ESP32

    User->>TG: нажатие inline-кнопки
    TG->>UDB: findByTelegramIdentity()
    UDB-->>TG: user + plc_username
    TG->>ACL: sanitizeSummary / canSendControllerCommand
    ACL-->>TG: ok / deny
    TG->>DWS: sendCmd() / sendGet()
    DWS->>PLC: cmd / get
    PLC-->>DWS: ack / result
    TG-->>User: новое сообщение с меню
Loading

Dependency Injection

Сборка приложения построена через awilix.

Точка входа:

Composition root:

Основные сервисы контейнера:

  • SessionStore
  • DeviceRegistry
  • UsersDb
  • DevicesDb
  • TelegramConfigDb
  • TelegramBotService
  • DatastoreFactory
  • HttpFactory
  • WsFactory
  • AppServer

Структура проекта

src/
  app/
    AppContainer.js
    AppServer.js
    factories/
  auth/
    AccessControl.js
  bot/
    TelegramBotService.js
    menu/
  db/
  http/
  state/
  utils/
  ws/
public/
  index.html
  styles.css
  js/
data/
proto.json

Данные

Основное хранилище

Основная БД проекта:

  • data/plc-cloud.sqlite

В ней лежат:

  • объекты и устройства
  • пользователи
  • Telegram-конфиг

Legacy-импорт

При первом запуске код умеет импортировать старые JSON-файлы:

  • data/users.json
  • data/devices.json
  • data/telegram.json

После импорта основным источником истины становится SQLite.

Пользователи

Пользователь хранит не только логин и пароль, но и Telegram/ACL-поля:

  • username
  • password_hash
  • plc_username
  • telegram_username
  • chat_id
  • telegram_notify_online
  • telegram_notify_offline
  • telegram_notify_events
  • notification_prefs_json
  • allowed_objects_json

Переменные окружения

Основные:

  • HOST
  • WEB_PORT
  • PORT
  • PLC_CLOUD_SEED_SAMPLE=1
  • TELEGRAM_BOT_TOKEN

Legacy Telegram env-переменные, которые ещё поддерживаются как defaults для storage:

  • TELEGRAM_BOT_PUBLIC_BASE_URL
  • TELEGRAM_BOT_WEBHOOK_PATH
  • TELEGRAM_BOT_SECRET_TOKEN

Установка

npm install

Запуск

npm start

Режим разработки:

npm run dev

Docker

Сборка образа:

docker build -t plc-cloud .

Запуск контейнера:

docker run -d \
  --name plc-cloud \
  -p 80:80 \
  -p 3001:3001 \
  -v "$(pwd)/data:/app/data" \
  -e HOST=0.0.0.0 \
  -e WEB_PORT=80 \
  -e PORT=3001 \
  plc-cloud

Запуск через compose:

docker compose up -d --build

Текущий маппинг compose:

  • ${WEB_HOST_PORT:-80}:80
  • ${DEVICE_HOST_PORT:-3001}:3001

REST API

Основные маршруты:

  • POST /api/login
  • POST /api/logout
  • GET /api/objects
  • GET /api/devices?object=<name>
  • GET /api/device/:id
  • GET /api/admin/devices
  • GET /api/admin/users
  • POST /api/admin/users
  • PUT /api/admin/users/:username
  • DELETE /api/admin/users/:username
  • GET /api/admin/telegram/settings
  • PUT /api/admin/telegram/settings
  • CRUD объектов
  • CRUD устройств

Browser WebSocket API

Endpoint:

  • /ws/web

Сообщения:

  • list_devices
  • subscribe_device
  • unsubscribe_device
  • send_get
  • send_cmd

Broadcasts:

  • device_online
  • device_offline
  • device_update
  • command_sent
  • command_error

Все snapshot'ы, которые уходят в браузер, проходят ACL-санацию.

Device WebSocket API

Endpoint:

  • /ws/device

Поддерживаемый цикл:

  • hello
  • hello_ack
  • result
  • ack
  • event
  • ping/pong

После hello все сообщения должны нести валидный session_id.

Telegram

🤖 long polling   🧭 inline-меню   🔐 ACL-фильтрация   🔔 уведомления

Текущий режим:

  • long polling
  • webhook не нужен

Текущая интерактивная структура:

  • объекты
  • устройства и stack slave-узлы
  • контроллеры
  • розетки
  • свет
  • метео
  • термостаты
  • баки
  • септик
  • полив
  • быстрые действия

Поведение меню:

  • long polling
  • inline-кнопки
  • бот отправляет новый экран сообщением, а не редактирует текущее сообщение
  • в настройках облака показываются Last Chat ID, последний username и время последней активности

Уведомления уходят только тем пользователям, у которых:

  • заполнен chat_id
  • заполнен plc_username
  • ACL разрешает доступ к устройству

Фронтенд

SPA включает:

  • выбор объектов
  • список устройств
  • статус устройства
  • обзор контроллеров
  • розетки
  • свет
  • метео
  • термостаты
  • баки
  • септик
  • полив
  • сеть
  • настройки с плитками:
    • объекты
    • устройства
    • пользователи
    • телеграм

ACL

ACL приходит от PLC и интерпретируется в:

Используется в:

  • REST API
  • browser websocket
  • Telegram bot

Если ACL отсутствует, код работает в legacy permissive-режиме.

Ограничения

ℹ️ Проект использует SQLite для persistent-данных и живой runtime state в памяти.

  • браузерные сессии хранятся только в памяти
  • online runtime state хранятся только в памяти
  • long polling предполагает один активный экземпляр бота на токен
  • Telegram storage всё ещё содержит legacy webhook-поля
  • локальный node на этой машине может падать из-за отсутствующего icu4c; для проверок безопаснее использовать Docker node:20-alpine

Как расширять проект

Чтобы добавить новый контроллер:

  1. расширить proto.json
  2. при необходимости согласовать контракт с прошивкой PLC
  3. обновить src/auth/AccessControl.js
  4. добавить рендеринг во public/js/ui.js
  5. добавить действия во public/js/main.js
  6. при необходимости добавить Telegram-меню в src/bot/menu

Скриншоты

About

Cloud server for PLC

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors