Перейти к содержанию

Архитектура системы OKTO

Подробное техническое описание всех компонентов платформы прослеживаемости OKTO: edge-сервис, центральный локальный сервер, центральная консоль, интеграция с облаком OKTO.

Аудитория: инженеры, DevOps, архитекторы. Уровень детализации: средний-глубокий — конкретные классы, таблицы, пакеты, очереди. Связанные документы: SERVER_MANAGEMENT.ru.md, API_REFERENCE.ru.md, DEPLOYMENT.ru.md, DEVELOPER_GUIDE.ru.md.


Содержание

  1. Обзор
  2. Топология развёртывания
  3. Состав компонентов
  4. Технологический стек
  5. Структура репозитория
  6. Ключевые сервисы edge-сервиса
  7. Ключевые сервисы центрального сервера
  8. Модель данных
  9. Поток данных: производство → облако
  10. Режимы подключения
  11. Управление устройствами (WebSocket)
  12. Безопасность и аутентификация
  13. Параллелизм и потоки
  14. Наблюдаемость
  15. Фронтенд-архитектура
  16. Ограничения и известные компромиссы
  17. MARS L2 (WET/DRY) — шкафы, драйверы, журнал

1. Обзор

OKTO — это распределённая система прослеживаемости для производственных линий, в которой каждое граничное устройство (edge) выполняет сканирование, печать, агрегацию и ведение локальной очереди, а центральный локальный сервер (ЦЛС) оркеструет парк устройств на площадке и отвечает за синхронизацию данных с облаком OKTO.

Основные функциональные блоки:

  • Ввод кодов: сканирование DataMatrix / EAN на линии, валидация по регуляторным правилам.
  • Агрегация: бутылка → короб (батч) → паллета с генерацией кодов SSCC.
  • Управление оборудованием: принтеры (Videojet, Markem, Solmark, Lineko, Domino, ZPL), сканеры (TCP / HID), ПЛК (MODBUS TCP).
  • Хранение и очередь offline: SQLite на edge, PostgreSQL на ЦЛС.
  • Удалённое управление: команды, прошивки, конфигурация, телеметрия по WebSocket.
  • Облачная интеграция: выгрузка агрегированных данных в OKTO Cloud через HTTPS.

Система рассчитана на работу в двух режимах:

Режим Путь данных Применимость
DIRECT_CLOUD edge → OKTO Cloud Одиночный терминал, слабая инфраструктура на площадке.
VIA_LOCAL_SERVER edge → ЦЛС → OKTO Cloud Многотерминальные заводы, требование буфера, аудита, массового OTA.

2. Топология развёртывания

 ┌────────────────────────────────────────────────────────────────────────────┐
 │                              OKTO Cloud                                    │
 │                           https://app.okto.ru                              │
 │   /companies/{id}/bottles   /batches   /pallets   /batches/fixate          │
 │   Реестр кодов · Аналитика · Соответствие регуляторам                      │
 └────────────▲───────────────────────────────────────────────────────────────┘
              │ HTTPS bearer token
              │ (CloudSyncService или прямая отправка с edge)
 ┌────────────┴───────────────────────────────────────────┐
 │   Центральный локальный сервер (Ktor + PostgreSQL)     │
 │                                                        │
 │   FactoryCloudClient   ─▶ очередь cloud_sync_queue ──▶ │
 │   EdgeSyncService      (персистит + ставит в очередь)  │
 │   DeviceConnectionRegistry  (живые WS-сессии)          │
 │   CommandDispatchService    (команды + результаты)     │
 │   FirmwareService           (загрузка/деплой)          │
 │   AuthService + JwtService  (HS256)                    │
 │   ConnectionModeManagementService                      │
 │   /api/v1/* (REST)                                     │
 │   /ws/device   (WS для устройств)                      │
 │   /ws/dashboard (WS для консоли)                       │
 └────────────▲─────────────────────▲─────────────────────┘
              │ POST /api/v1/sync   │ WS /ws/device (команды + события)
              │ (VIA_LOCAL_SERVER)  │
 ┌────────────┴─────────────────────┴──────────────────────┐
 │   Edge Service (Kotlin/Ktor + SQLite)                   │
 │                                                         │
 │   ServerConnectionService  (persistent WS клиент)       │
 │   CommandHandlerService    (исполнение команд)          │
 │   OfflineQueueService      (retry/forwarder)            │
 │   BottleService · BatchService · PalletService          │
 │   PrinterManager · ScannerManager · ModbusClient        │
 │   LocalOperatorAPI (REST/WS на 8080 для оператора UI)   │
 └─────────────────────────────────────────────────────────┘
 ┌────────────▼────────────────┐
 │  Оператор-UI (браузер/киоск) │
 │  Операционная панель линии   │
 └──────────────────────────────┘

Одновременно в одной площадке:

  • 1 экземпляр ЦЛС (один Ktor-процесс + один PostgreSQL).
  • N граничных edge-сервисов (по одному на терминал / линию).
  • 1–N рабочих мест управления — Центральная консоль (React SPA), подключается к ЦЛС через REST и /ws/dashboard.

3. Состав компонентов

Модуль Язык / фреймворк Назначение
common/ Kotlin JVM 17 Общие доменные модели (Bottle, Batch, Pallet, Device, DeviceCommand…), DTO, утилиты, валидация.
edge-service/ Kotlin + Ktor Netty Граничный сервис: локальное API для оператора, клиент ЦЛС, работа с оборудованием.
factory-server/ Kotlin + Ktor Netty + Exposed + HikariCP Центральный локальный сервер.
management-dashboard/ React 18 + TypeScript + Vite + MUI + TanStack Query + react-i18next Центральная консоль (SPA).
operator-ui/ React + Vite Операторский интерфейс на едж-терминале.
packaging/ systemd unit-файлы, скрипты, sudoers Пакетирование и привилегии OS для OTA, reboot.
docker/ Dockerfile + compose Контейнеризация.
docs/ Markdown Документация.

4. Технологический стек

Бэкенд

  • Язык: Kotlin 1.9, JVM 17.
  • HTTP/WS: Ktor Netty (blocking-friendly с корутинами).
  • Сериализация: kotlinx.serialization с classDiscriminator = "type" для sealed-иерархий.
  • ORM: Exposed (DSL + DAO), PostgreSQL диалект.
  • Пулл соединений: HikariCP.
  • DI: Koin.
  • Конфигурация: Hoplite (YAML + env override).
  • Логи: Logback + SLF4J, ротация, JSON-форматтер по флагу.
  • Метрики и трейсы: OpenTelemetry → OTLP экспортёр на localhost:4317, Prometheus /metrics на :9091.
  • Тесты: JUnit 5, MockK, Testcontainers (PostgreSQL), JaCoCo.

Edge-сервис — дополнительно

  • Локальная БД: SQLite (через Exposed JDBC).
  • Оборудование:
  • Принтеры: TCP-сокеты + специфичные протоколы (см. edge-service/src/main/kotlin/ru/okto/edge/hardware).
  • Сканеры: TCP или HID-клавиатурная эмуляция.
  • ПЛК: Modbus TCP (библиотека jamod / собственный клиент).

Фронтенд

  • React 18, TypeScript 5, Vite 5.
  • UI-кит: MUI 5 + собственные токены дизайна в src/ui/tokens.ts.
  • Роутинг: React Router.
  • Сеть: Axios + собственный клиент с JWT-инжектором.
  • Данные: TanStack Query (кэш + фоновое обновление).
  • Состояние: локальный useState + Zustand (минимально, для авто-статуса и палитры команд).
  • Реальное время: собственный WS-обёртыватель с auto-reconnect.
  • i18n: react-i18next, локали ru.json / en.json, по умолчанию — русский.

5. Структура репозитория

okto-android-to-linux/
├── common/                          — общие модели и контракты
│   └── src/main/kotlin/ru/okto/common/
│       ├── api/             — DeviceControl.kt (команды/события), ApiContracts.kt
│       ├── domain/          — Bottle, Batch, Pallet, Device, ConnectionMode…
│       ├── cloud/           — DTO облачных запросов
│       └── util/, validation/
├── edge-service/                    — граничный сервис
│   └── src/main/kotlin/ru/okto/edge/
│       ├── Application.kt   — точка входа, Koin, Ktor плагины, запуск фоновых сервисов
│       ├── api/             — Routes.kt (REST для оператора), Plugins.kt
│       ├── cloud/           — OktoCloudClient (DIRECT_CLOUD)
│       ├── sync/            — FactoryServerSyncClient (VIA_LOCAL_SERVER)
│       ├── service/         — сервисы (см. §6)
│       ├── hardware/        — драйверы принтеров/сканеров/PLC
│       ├── persistence/     — Exposed-таблицы SQLite, репозитории
│       └── config/          — AppConfig.kt и подструктуры
├── factory-server/                  — центральный локальный сервер
│   └── src/main/kotlin/ru/okto/factory/
│       ├── Application.kt   — точка входа
│       ├── api/
│       │   ├── AuthRoutes.kt              — логин/логаут/юзеры
│       │   ├── Routes.kt                  — устройства, sync, dashboard REST
│       │   └── ServerManagementRoutes.kt  — команды/прошивки/группы/WS
│       ├── service/
│       │   ├── AuthService.kt
│       │   ├── JwtService.kt
│       │   ├── DeviceConnectionRegistry.kt
│       │   ├── CommandDispatchService.kt
│       │   ├── FirmwareService.kt
│       │   ├── ConnectionModeManagementService.kt
│       │   └── Services.kt (DashboardService, EdgeSyncService)
│       ├── persistence/
│       │   ├── Database.kt                 — HikariCP + схема
│       │   ├── Tables.kt                   — все Exposed-таблицы
│       │   ├── AuthTables.kt               — пользователи/сессии/аудит
│       │   ├── DeviceRepository.kt
│       │   ├── DeviceCommandRepository.kt  — команды/логи/группы/прошивки/конфиг
│       │   ├── AggregationRepository.kt    — агрегированные бутылки/батчи/паллеты
│       │   ├── SyncRepository.kt           — cloud_sync_queue
│       │   └── GlobalConfigRepository.kt
│       ├── sync/
│       │   ├── CloudSyncService.kt         — фоновый обработчик очереди
│       │   └── FactoryCloudClient.kt       — HTTP-клиент к OKTO Cloud
│       └── config/AppConfig.kt
├── management-dashboard/            — Центральная консоль (React SPA)
│   ├── src/
│   │   ├── api/              — клиенты (devices, commands, firmware, auth)
│   │   ├── auth/AuthContext.tsx
│   │   ├── hooks/            — useLiveEvents, useRelativeTime, useCountUp, useProductionHistory
│   │   ├── pages/            — Overview, Devices, DeviceDetail, Firmware, Groups, Connection, Audit, Users, Preferences, Sync, Login
│   │   ├── ui/               — toolkit (Card, StatCard, Sparkline, AreaChart, Illustration, ConfettiOverlay…)
│   │   └── i18n/locales/     — ru.json, en.json
├── operator-ui/                     — оператор-UI (React)
├── docker/                          — Dockerfile + docker-compose*.yml
├── packaging/                       — systemd + sudoers + скрипты
└── docs/                            — документация

6. Ключевые сервисы edge-сервиса

Сервис Файл Ответственность
ServerConnectionService service/ServerConnectionService.kt Постоянное WS-соединение с ЦЛС (/ws/device). Heart-beat, экспоненциальный backoff на реконнекте, инжектирование CommandHandlerService для входящих команд. Стартует только если режим VIA_LOCAL_SERVER.
CommandHandlerService service/CommandHandlerService.kt Диспетчер входящих команд: force_sync, pull_logs, restart_service, reboot_os, shutdown_os, update_firmware, push_config, exec_shell, enable_device/disable_device. Выполняет, возвращает CommandResult.
OfflineQueueService service/OfflineQueueService.kt Периодический pusher SQLite-очереди (operation_queue) — либо в ЦЛС (через FactoryServerSyncClient), либо напрямую в облако (через OktoCloudClient). Поддерживает triggerImmediate() и clearQueue() для команд.
BottleService / BatchService / PalletService / CopackingService service/…Service.kt Бизнес-операции агрегации. После успешной локальной записи дёргают offlineQueueService?.enqueue*.
HardwareStatusService service/HardwareStatusService.kt Периодический опрос принтеров/сканеров, агрегирует статус в StatusEvent.
ConnectionModeService service/ConnectionModeService.kt Получает/меняет текущий режим (push от сервера или локальный override).
DeviceProvisioningService service/DeviceProvisioningService.kt Первый bootstrap: получение device JWT через POST /api/v1/devices/{id}/token с X-Enrollment-Key.
OktoCloudClient cloud/OktoCloudClient.kt REST-клиент к облаку (для DIRECT_CLOUD).
FactoryServerSyncClient sync/FactoryServerSyncClient.kt REST-клиент к ЦЛС POST /api/v1/sync. Разворачивает обёртку ApiResponse<SyncResponse>.
InMemoryDeviceConfigStore service/CommandHandlerService.kt In-memory хранилище patch-ей от push_config. Сбрасывается при рестарте; для durable-конфигурации смотрите раздел Ограничения.

Точка входа — Application.kt:

  1. Hoplite читает YAML + env.
  2. Запускается Ktor Netty на server.port.
  3. Koin-модуль собирает граф зависимостей.
  4. После старта startBackgroundServices() поднимает OfflineQueueService и ServerConnectionService (если режим VIA_LOCAL_SERVER). Также подставляет BottleService.offlineQueueService через property-setter (лейзи-обход циклической зависимости).

7. Ключевые сервисы центрального сервера

Сервис Файл Ответственность
AuthService service/AuthService.kt Логин/логаут, проверка legacy-HMAC токенов (для бэккомпат), управление пользователями и терминалами, запись в audit log.
JwtService service/JwtService.kt Выпуск и проверка HS256-подписанных JWT — пользовательских (scope=user, role) и устройственных (scope=device).
DeviceConnectionRegistry service/DeviceConnectionRegistry.kt In-memory ConcurrentHashMap<deviceId, DeviceSession> живых WS-сессий. Shared-flow inbound для всех входящих фреймов.
CommandDispatchService service/CommandDispatchService.kt Персистит команду → отправляет в DeviceConnectionRegistry → await-ит CommandResult с таймаутом → публикует события в eventBus для /ws/dashboard. Поддерживает dispatch(deviceId) и dispatchToGroup(groupId).
FirmwareService service/FirmwareService.kt Приём артефакта (stream в data/firmware/), SHA-256, персист в firmware_releases. deploy(release, deviceIds) рассылает UpdateFirmwareCmd.
ConnectionModeManagementService service/ConnectionModeManagementService.kt Читает/пишет глобальные настройки (GlobalConfigTable) и per-device override. Подсчитывает статистику для /connection-mode.
DashboardService service/Services.kt REST-ручки для сводки: onlineDevices, bottlesProcessedToday, batchesCreatedToday, cloudQueueStats. Берёт данные через AggregationRepository + DeviceRepository.
EdgeSyncService service/Services.kt Приёмник POST /api/v1/sync от edge: декодирует SyncOperation, сохраняет в aggregated_*, ставит в cloud_sync_queue, возвращает SyncResponse. Логирует в sync_log.
CloudSyncService sync/CloudSyncService.kt Фоновый (coroutine) обработчик cloud_sync_queue. Берёт задачу с наивысшим приоритетом, находит устройство, резолвит companyId, собирает нужный DTO (CloudBottleModel / CloudBatchModel / CloudPalletModel / CloudTemporaryBatchFixate), вызывает FactoryCloudClient, применяет backoff при ошибке.
FactoryCloudClient sync/FactoryCloudClient.kt HTTP-клиент к облаку. Bearer token из cloudSync.authToken. Унифицированные методы sendBottle, sendBatch, sendPallet, fixateTemporaryBatch, возвращают CloudApiResult.

Все сервисы связаны через Koin (appModule в Application.kt). Бэкграунд-корутины запускаются в startBackgroundServices().


8. Модель данных

8.1 Схема PostgreSQL (центральный сервер)

Таблицы (все определения — в factory-server/src/main/kotlin/ru/okto/factory/persistence/Tables.kt):

Таблица Назначение Ключевые поля
devices Реестр устройств identifier PK, connection_mode, connection_mode_override, enabled, group_id, firmware_version, last_heartbeat, last_command_at
companies Компании-операторы id PK, fsrar_id, time_zone
production_lines Производственные линии id PK, company_id FK, party, template
products Продукция id PK, gtin_number, bottles_in_batch, batches_in_pallet
aggregated_bottles Синхронизированные бутылки identifier PK, device_id FK, excise_duty_number UNIQUE, status, synced_at
aggregated_batches Батчи identifier PK, device_id FK, bottle_count, status
aggregated_pallets Паллеты identifier PK, device_id FK, batch_count
sync_log Лог приёма sync-операций id autoinc, operation_type, record_count, success
cloud_sync_queue Очередь на облако id PK, type, payload, status, retry_count, priority, source_device_id, scheduled_at
copacking_tasks Задания на копакинг id PK, status, target_quantity, completed_quantity
device_metrics Исторические метрики id autoinc, device_id, cpu_usage, memory_usage, timestamp
alerts Системные алерты id, severity, category, acknowledged
global_config Глобальные настройки key PK (default_connection_mode, allow_device_mode_override, cloud_endpoint, cloud_api_key)
device_commands История команд id PK, device_id, type, payload_json, status, timeout_ms, result_json
device_logs Стрим логов устройств id autoinc, device_id, ts, level, line, command_id
device_groups / device_group_members Логические группы для массовых операций group_id + device_id композитный PK в members
firmware_releases Артефакты прошивок id PK, version UNIQUE, channel, sha256, size_bytes, artifact_url
firmware_deployments Per-device статусы деплоя id PK, release_id FK, device_id, status (PENDING/IN_PROGRESS/SUCCESS/FAILED/TIMEOUT)
device_configs Желаемая конфигурация устройства device_id PK, config_json, version

Таблицы аутентификации (в AuthTables.kt):

  • users — учётки консоли: id, username, password_hash (bcrypt / pbkdf2), role (ADMIN / MANAGER / OPERATOR / VIEWER), disabled.
  • terminals — терминалы (CRUD в настройках).
  • user_terminals — связь пользователь ↔ терминал (scope-ирование доступа).
  • sessions — HMAC-токены легаси-сессий.
  • audit_log — каждое привилегированное действие: actor_user_id, action, entity_type, entity_id, ip, meta_json, ts.

Схема создаётся/мигрируется в Database.init() вызовом SchemaUtils.create(...). Миграции отсутствуют как формальный механизм — для продакшна см. раздел DEPLOYMENT.ru.md → Миграции.

8.2 Схема SQLite (edge)

На edge-устройстве локально хранятся:

  • bottles, batches, pallets, copacking_tasks — рабочие доменные данные.
  • operation_queue — очередь операций на отправку в ЦЛС/облако:
  • id, type (BOTTLE_CREATE / BATCH_CREATE / PALLET_CREATE / BATCH_FIXATE…),
  • payload (JSON), status (PENDING / IN_PROGRESS / DONE / FAILED),
  • retry_count, last_error, scheduled_at.
  • printers, scanners, modbus_config — кэш конфигурации оборудования.
  • users, sessions — локальные учётки оператора.

9. Поток данных: производство → облако

9.1 Режим VIA_LOCAL_SERVER

(сканер) → BottleService.createBottles([...])
           ├──1. INSERT INTO bottles (SQLite)
           └──2. OfflineQueueService.enqueueBottleCreate(bottle)
                  └── INSERT INTO operation_queue (type=BOTTLE_CREATE, payload=..., status=PENDING)

[каждые syncIntervalMs]
OfflineQueueService.tick()
  └── fetch PENDING batch → POST /api/v1/sync (FactoryServerSyncClient)
EdgeSyncService.processSync(request)
  ├── для каждой SyncOperation:
  │     ├── persistOperation() → INSERT INTO aggregated_bottles/… (дедуп по unique)
  │     └── enqueueForCloud()  → INSERT INTO cloud_sync_queue
  └── SyncResponse { accepted=[…], rejected=[…] }

[фоновая корутина]
CloudSyncService.loop()
  └── SELECT … FROM cloud_sync_queue WHERE status=PENDING ORDER BY priority, scheduled_at LIMIT batchSize
        ├── для каждой записи:
        │     ├── собрать CloudBottleModel/Batch/Pallet
        │     ├── FactoryCloudClient.sendBottle/… → https://app.okto.ru/…
        │     ├── SUCCESS → UPDATE cloud_sync_queue SET status=DONE
        │     └── FAILURE → retry_count++ и backoff (экспоненциально), status=DEAD_LETTER после maxRetries

9.2 Режим DIRECT_CLOUD

Тот же путь, но на шаге «OfflineQueueService.tick()» устройство шлёт напрямую в OktoCloudClient (HTTPS к cloud.okto.ru). ЦЛС в цепочке не участвует.

9.3 Обратная совместимость

Если POST /api/v1/sync вернул ошибку сети / 5xx, edge оставляет операцию со статусом PENDING и ретрит по расписанию. Облако OKTO идемпотентно принимает повторы по identifier бутылки / exciseDutyNumber — дубликаты на облачной стороне отсекаются.


10. Режимы подключения

Каждое устройство имеет атрибут connection_mode (DIRECT_CLOUD или VIA_LOCAL_SERVER). Алгоритм выбора:

if device.connection_mode_override:
    mode = device.connection_mode
else:
    mode = GlobalConfigTable[default_connection_mode]

Администратор может:

  1. Задать глобальный дефолт (UI → «Режим подключения»).
  2. Включить/выключить device override (флаг allow_device_mode_override).
  3. Переопределить режим для конкретного устройства (кнопка в карточке устройства).

Edge-сервис читает режим при старте из конфига, но при ConnectionMode change с сервера (через WS) переключает клиенты «на лету»:

  • DIRECT_CLOUDServerConnectionService.stop() + OfflineQueueService.useCloudClient().
  • VIA_LOCAL_SERVERServerConnectionService.start() + OfflineQueueService.useFactoryClient().

11. Управление устройствами (WebSocket)

Протокол определён в common/api/DeviceControl.kt и детально описан в SERVER_MANAGEMENT.ru.md.

Каналы

Канал Кто слушает Кто подключается Аутентификация
/ws/device?token=<deviceJwt> ЦЛС (ServerManagementRoutes.kt) edge-сервис device JWT (scope=device)
/ws/dashboard?token=<userJwt> ЦЛС центральная консоль user JWT (scope=user)

Типы сообщений

Server → Device (sealed interface ServerToDeviceMessage):

  • DeviceCommand — управляющая команда (подробно в §11 SERVER_MANAGEMENT.ru.md):
  • RestartServiceCmd, RebootOsCmd, ShutdownOsCmd
  • ForceSyncCmd, ClearQueueCmd
  • PullLogsCmd { tailLines, level }
  • PushConfigCmd { patchJson }
  • UpdateFirmwareCmd { releaseId, url, sha256, version }
  • ExecShellCmd { templateId, args }
  • EnableDeviceCmd, DisableDeviceCmd { reason }

Device → Server (sealed interface DeviceToServerMessage):

  • DeviceHelloMessage { deviceId, version, bootTs }
  • CommandResult { commandId, success, output, error, data? }
  • CommandProgress { commandId, percent, message }
  • StatusEvent { status, metrics }
  • LogLineEvent { ts, level, line, commandId? }
  • DeviceScanEvent { code, valid }
  • DevicePrintEvent { code }
  • DeviceAlertEvent { severity, message }

Dashboard → Server (sealed interface DashboardCommand):

  • DashboardSubscribe { deviceIds?, eventTypes? } — фильтр потока.
  • DashboardUnsubscribe.

Жизненный цикл WS-команды

[UI] POST /api/v1/devices/D1/commands {type:"force_sync"}
     ├─ CommandDispatchService.dispatch("D1", cmd, user, 15000ms)
     │   ├─ 1. INSERT INTO device_commands (status=PENDING)
     │   ├─ 2. registry.send("D1", cmd) → WS text frame
     │   ├─ 3. UPDATE device_commands SET status=DISPATCHED, dispatched_at=now()
     │   ├─ 4. await deferred<CommandResult>  (timeout 15000ms)
     │   │     ├─ при получении DeviceToServerMessage.CommandResult с matching id:
     │   │     │    deferred.complete(result)
     │   │     └─ при timeout: UPDATE SET status=TIMEOUT
     │   └─ return CommandResult → HTTP 200
     └─ audit_log: DEVICE_COMMAND_DISPATCHED

12. Безопасность и аутентификация

JWT

  • Алгоритм: HS256, секрет auth.jwtSecret (обязательно сменить в продакшне).
  • User JWT: sub=userId, scope=user, role ∈ {ADMIN, MANAGER, OPERATOR, VIEWER}, TTL auth.tokenExpirationMs (24h дефолт).
  • Device JWT: sub=deviceId, scope=device, TTL ~1 год.

Bootstrap устройства (enrollment)

edge (первый запуск)
  │ POST /api/v1/devices/{id}/token
  │  X-Enrollment-Key: <shared secret>
  │  ?name=…&companyId=…&productionLineId=…&version=…
factory
  ├─ if auth.deviceEnrollmentKey != header → 401
  ├─ if device не найден AND auth.allowAutoEnrollment=true → INSERT INTO devices(status=OFFLINE)
  └─ return { token: <deviceJwt> }

edge сохраняет token в локальный keystore (SQLite `device_auth`),
затем использует для /ws/device и для будущих POST /api/v1/sync.

RBAC

Роли и допустимые действия (enforced в хэндлерах AuthRoutes.kt / ServerManagementRoutes.kt):

Действие ADMIN MANAGER OPERATOR VIEWER
Логин
Список устройств / команд / аудита
Dispatch force_sync, pull_logs, enable_device
Dispatch clear_queue
Dispatch restart_service
Dispatch disable_device, reboot_os, shutdown_os
push_config, update_firmware
Upload прошивки, деплой OTA
Управление пользователями и терминалами
Глобальные настройки подключения

Привилегии OS на edge

Для reboot_os / shutdown_os / restart_service / OTA-swap требуется sudoers-файл packaging/sudoers/okto:

okto ALL=(root) NOPASSWD: /bin/systemctl reboot, /bin/systemctl poweroff, \
                           /bin/systemctl restart okto-edge, \
                           /bin/systemctl start okto-edge-update, \
                           /usr/local/bin/okto-edge-swap-firmware

Без него команды возвращают non-zero exit code.


13. Параллелизм и потоки

Edge-сервис

  • Ktor Netty event-loop обрабатывает REST-запросы (≤ 2 worker threads для малых нагрузок).
  • Scheduler-корутины (CoroutineScope(SupervisorJob() + Dispatchers.IO)):
  • OfflineQueueService.loop() — каждые sync.intervalMs.
  • ServerConnectionService.runSession() — реконнект с backoff.
  • HardwareStatusService.loop() — каждые hardware.pollIntervalMs.
  • Hardware-потоки:
  • PrinterClient — dedicated thread / корутина на каждый принтер.
  • ScannerClient — аналогично.
  • ModbusClient — polling-поток.

Центральный сервер

  • Ktor Netty event-loop + Dispatchers.IO для блокирующих БД-операций.
  • CommandDispatchService использует Channel<CommandResult> для сопоставления результатов.
  • DeviceConnectionRegistry.inboundMutableSharedFlow<Pair<deviceId, DeviceToServerMessage>> с replay=0.
  • CloudSyncService — одна корутина с syncIntervalMs тиком, последовательно обрабатывает batchSize задач.

Блокировки

  • Избегаем длинных synchronized блоков. Вместо этого — Mutex / AtomicReference / ConcurrentHashMap.
  • Exposed-транзакции оборачиваются в dbQuery { transaction { … } } с Dispatchers.IO.
  • WS-отправка проходит через session.send(Frame.Text(…)) — не blocking, но при backpressure возможна очередь.

14. Наблюдаемость

Метрики

  • Prometheus HTTP endpoint: http://<host>:9091/metrics.
  • Основные counters/gauges:
  • http_server_requests_seconds — латентность REST.
  • okto_devices_online — число активных WS-сессий.
  • okto_cloud_sync_queue_size{status} — глубина очереди по статусам.
  • okto_commands_total{type,status} — команды.
  • okto_firmware_deployments_total{status} — деплои.
  • jvm_* — стандартные JVM-метрики (память, GC).

Трейсинг

  • OpenTelemetry OTLP exporter: tracing.otlpEndpoint (по умолчанию http://localhost:4317).
  • Автоматические спаны для Ktor-запросов + ручные спаны в CommandDispatchService, CloudSyncService.

Логи

  • Logback с rolling file appender (data/logs/).
  • Уровни: INFO в продакшне, DEBUG — для разработки (logging.level=DEBUG).
  • Формат: %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n.
  • Для централизованного логирования запустите Fluent Bit / Vector рядом и отгружайте в Loki/Elasticsearch.

Health-чеки

  • GET /health — ливенс без аутентификации (возвращает 200 если Ktor жив).
  • GET /api/v1/health — ready (проверяет доступность БД).
  • GET /api/v1/version — версия сборки.

15. Фронтенд-архитектура

management-dashboard/src/
├── App.tsx                   — QueryClientProvider, i18n init, Router, AuthProvider, ThemeProvider
├── main.tsx                  — React mount, импорт global.css
├── api/
│   ├── client.ts             — axios + token interceptor + openWs() обёртка с auto-reconnect
│   ├── devices.ts            — /devices, /overview, /connected, metrics, queue stats
│   ├── commands.ts           — /commands, dispatch, list, fetchDeviceLogs
│   ├── firmware.ts           — /firmware/releases, /deployments
│   └── auth.ts               — /auth/login, /auth/me
├── auth/
│   └── AuthContext.tsx       — {user, login, logout, factoryStatus}; authStore как non-React getter
├── hooks/
│   ├── useLiveEvents.ts      — подписка на /ws/dashboard, rolling buffer
│   ├── useRelativeTime.ts    — "3 мин назад" форматтер с i18n
│   ├── useCountUp.ts         — плавная анимация числовых стат-карточек
│   └── useProductionHistory.ts — скользящее окно сэмплов для графика
├── ui/
│   ├── tokens.ts             — colors, space, radius, font, shadow, motion, z, layout
│   ├── theme.ts              — MUI theme (shape.borderRadius=1, кастомные компоненты)
│   └── components/           — AppShell, PageHeader, Card, StatCard, Badge, Sparkline,
│                               AreaChart, EmptyState, Illustration, Toaster, ConfirmDialog,
│                               CommandPalette, ConfettiOverlay, StatusDot, Skeleton
├── pages/                    — Overview, Devices, DeviceDetail, Firmware,
│                               DeviceGroups, Connection, Audit, Users, Preferences,
│                               Sync, Login
├── i18n/
│   ├── index.ts              — lng='ru', fallbackLng='ru', setLanguage() helper
│   └── locales/ru.json, en.json
└── styles/global.css

Поток данных

  • REST: TanStack Query → api/* клиенты → Axios → /api/v1/* с JWT.
  • Realtime: useLiveEventsopenWs('/ws/dashboard?token=…') → reconnect loop + subscription-сообщение при open.
  • Auth: AuthContext хранит user + token. Token сохраняется в localStorage (okto.jwt). При 401 → clearAuth + redirect /login.

i18n

  • По умолчанию русский (lng: 'ru', fallbackLng: 'ru').
  • Переключение через PreferencessetLanguage('en' | 'ru') → запись localStorage.okto.lang + document.documentElement.lang.
  • 600+ ключей в ru.json, сгруппированы по области UI (app, nav, topbar, overview, fleet, device, firmware, groups, connection, audit, users, prefs).

Canvas-подобные анимации

  • useCountUp — requestAnimationFrame + ease-out-cubic, prefers-reduced-motion aware.
  • ConfettiOverlay — MUI Modal + 18 частиц с CSS custom properties + single keyframe.
  • Роут-переходы — fade-in на key={location.pathname}.

16. Ограничения и известные компромиссы

Намеренные упрощения в текущей реализации:

  • Один экземпляр ЦЛС на площадку. DeviceConnectionRegistry — in-memory. Горизонтальное масштабирование требует шардинга (Redis Streams / Postgres LISTEN-NOTIFY / NATS), либо sticky-роутинга по deviceId.
  • InMemoryDeviceConfigStore на edgepush_config переживает только до рестарта JVM. Для durability нужно реализовать PersistentDeviceConfigStore, пишущий в SQLite-таблицу config_patches.
  • Hot-reload не полный. Большинство сервисов (scanner, printer, modbus, cloud client) читают конфигурацию при старте. После push_config требуется restart_service.
  • Device JWT в query-string /ws/device?token=… — значение попадает в access-логи reverse-proxy. В продакшне терминируйте TLS и стрипайте query, либо переведите на first-message auth.
  • Нет JWT revocation. Logout удаляет session row, но JWT живёт до exp. Для force-revoke поверните auth.jwtSecret. Per-user blacklist пока не реализован.
  • Cloud auth token статичен. cloudSync.authToken — долгоживущий bearer. Если OKTO Cloud перейдёт на OAuth / короткоживущие токены, оберните FactoryCloudClient рефрешером.
  • Нет автоматических миграций. Схема создаётся SchemaUtils.create(...) при старте. Для изменений — Flyway или ручные ALTER TABLE + обновление Tables.kt. См. DEPLOYMENT.ru.md.
  • Логи устройств (device_logs) не ротируются. Добавьте периодическую очистку: DELETE FROM device_logs WHERE ts < NOW() - INTERVAL '30 days'.
  • Прошивки не подписаны по умолчанию. Протокол содержит signatureBase64, но проверка Ed25519 не включена. Для supply-chain integrity встройте trusted public key в edge-JAR и включите проверку в UpdateFirmwareExecutor.
  • Привилегированные команды требуют sudoers. Без него reboot_os, shutdown_os, restart_service, update_firmware-swap вернут non-zero exit code. Docker без systemd полагается на restart_service + supervisor restart.

Для обсуждения эволюции и roadmap — смотрите CHANGELOG.md и открытые задачи в Linear.


17. MARS L2 (WET/DRY) — шкафы, драйверы, журнал

Расширение edge-сервиса для проекта партийного учёта MARS (апрель 2026). Полный анализ отличий — в L2_WET_DRY_GAP_ANALYSIS.md; план развёртывания — в ROLLOUT.ru.md.

17.1 Варианты исполнения

Вариант Состав шкафов Примечание
DRY ПромПК (IP65 моноблок) + Шкаф управления с ПЛК Inovance AM521-0808-TN/AM522, коммутатором Huawei S5735L-S8T4XV-V2, модульным распределением и резервом под Point-I/O HARDWARE_BOM_DRY.ru.md
WET ПромПК + Шкаф питания (ИБП ОВЕН ИБП120К, автоматы, резерв под Point-I/O). ПЛК — на линии, L2 обращается к нему через fieldbus HARDWARE_BOM_WET.ru.md

Идентификаторы площадки и варианта хранятся в line.variant / line.site конфигурации edge-service.yaml и прокидываются в таблицу devices на центральном сервере (колонки variant, site, ups_present, plc_model, ipc_model, cabinet_serial). Миграция Flyway — V20260418_01__l2_variants.sql.

17.2 Драйверный реестр

edge-service/src/main/kotlin/ru/okto/edge/device/
├── plc/                  — PlcClient + протоколы
│   ├── ModbusTcpPlcClient.kt       (обёртка над jlibmodbus)
│   ├── ModbusRtuPlcClient.kt       (RS-485 через jSerialComm)
│   ├── OpcUaPlcClient.kt           (Eclipse Milo, активируется при наличии деп-ов)
│   ├── EthernetIpPlcClient.kt      (libplctag через JNI)
│   ├── TcpSocketPlcClient.kt       (общий кадровый протокол)
│   └── ProfinetPlcClient.kt        (заглушка — рекомендуется MOXA MGate 5103)
├── scanner/drivers/      — сканеры
│   ├── CognexDataManClient.kt
│   ├── HikrobotIdClient.kt
│   ├── DatalogicMatrixClient.kt
│   ├── DatalogicHandheldClient.kt  (USB-HID через Linux evdev)
│   ├── ModbusScannerClient.kt
│   └── EthernetIpScannerClient.kt
└── printer/L2PrinterDrivers.kt      — Markem SDX60/65, Novexx, Videojet TTO,
                                       Mobile ZPL, Mobile TSPL, Loopback

Регистрация протоколов — PlcClientFactory.register(...) в registerBuiltInPlcClients(). Каждый сканер резолвится в ScannerDriverRegistry.create(...) по паре (transport, model). Принтеры — в PrinterClientFactory.create(...) по PrinterType.

17.3 Сервисы уровня edge

Сервис Файл Назначение
ScannerManagerService service/ScannerManagerService.kt Один ScannerManager, до 10 сканеров, replaceScanners(...) останавливает текущие и пересоздаёт
PrinterManagerService service/PrinterManagerService.kt Мульти-принтер: список, параметрическая схема (PrinterParamSchema), шаблоны
PlcManagerService service/PlcManagerService.kt Мульти-ПЛК, хелсчек, проверка связи, browse тегов (OPC UA)
PlcBindingService service/PlcBindingService.kt Преобразование тег ↔ доменное событие: PUSH_CODE_ADD/REMOVE, PUSH_AGGREGATE, READ_L2_STATE, WRITE_SYSTEM_PARAMS, UPS_ON_BATTERY, ACK_ALARM
ScanRoleRouter service/ScanRoleRouter.kt Маршрутизация сканов по роли: ADD → BottleService, REMOVE → BottleRepository.cancel, VERIFY_LOCAL_BUFFER → BatchAccountingService
UpsMonitorService service/UpsMonitorService.kt Опрос ИБП (OWEN IBP120K / NUT / PLC-mapped), формирование переходов UPS_ON_BATTERY / RESTORED / LOW_BATTERY / CRITICAL / SHUTDOWN_IMMINENT
GpioService service/GpioService.kt Кнопки + индикаторы через libgpiod (или USB-GPIO), жёстко смаплены на действия ACK_ALARM / RESET / START_PAUSE / EMERGENCY_STOP
EventService service/EventService.kt HMI-журнал: микросекундные метки, подтверждение оператором, экспорт CSV/JSON/XML
EventMessages service/EventMessages.kt Каталог известных кодов событий с русскоязычными шаблонами по умолчанию
BatchAccountingService service/BatchAccountingService.kt «Партионный учёт»: загрузка буфера из L3, сверка кодов, статистика
LogSinkService service/LogSinkService.kt Приёмники журнала: файл (работает), HTTP (работает), FTP/SMB/S3/Cloud (заглушки под зависимости фазы 11)

17.4 REST API (префикс /api/v1/)

Сканеры: GET/PUT /scanners, GET /scanners/{i}/status, POST /scanners/{i}/test.

Принтеры: GET/PUT /printers, GET /printers/schema, GET /printers/{i}/status, POST /printers/{i}/print, POST /printers/{i}/test, CRUD /printers/{i}/templates[/{name}].

ПЛК: GET/PUT /plcs, GET /plcs/{id}/health, POST /plcs/{id}/test, GET /plcs/{id}/tags, GET/PUT /plc/bindings.

ИБП: GET /ups/status, POST /ups/test, POST /ups/simulate.

GPIO: GET /gpio/status, POST /gpio/buttons/{name}/test, POST /gpio/leds/{name}/set.

Журнал: GET /events, POST /events, GET /events/{id}, POST /events/{id}/ack, POST /events/ack-all, GET /events/export?format=csv|json|xml.

Партионный учёт: POST /batch-accounting/buffer/load, GET /batch-accounting/buffer, DELETE /batch-accounting/buffer, POST /batch-accounting/verify.

17.5 Таблицы в БД edge-сервиса (фаза 2)

  • events (id BIGSERIAL, ts_us BIGINT, severity, category, source, code, message, message_key, metadata_json, device_id, line_id, acknowledged, ack_by, ack_at_us) — HMI-журнал.
  • batch_buffer (code PK, batch_identifier, gtin, loaded_at_us, consumed, consumed_at_us, consumed_by) — локальный буфер партии.
  • print_templates (id UUID PK, printer_index, name, content, checksum, created_at_us, created_by, selected) — библиотека шаблонов.
  • scanner_states, printer_states, plc_states — снимки состояния оборудования для StatusService + центральной консоли.

17.6 Сетевой протокол / WS

Канал /ws на edge расширен типом event — при каждом вызове EventService.record(...) подписчикам приходит структурированное сообщение, которое Frontend (useEvents) отображает в Журнале в реальном времени.

/ws/dashboard на центральном сервере прокидывает эти события на консоль (одно устройство + агрегированно).

17.7 Интернационализация

  • Интерфейс оператора (operator-ui) и центральная консоль (management-dashboard) — по умолчанию русский (i18n/index.ts, ключ okto.lang). Английский — полный резерв, переключается из UI настроек.
  • Русский текст всех событий хранится в EventsTable.message; UI использует EventsTable.message_key (l2.events.*) для локализации по выбранному языку.

17.8 Миграция с Android-L2

  • CLI okto-migrate (packages/okto-migrate/) конвертирует SharedPreferences в edge-service.yaml и экспортирует историю SQLite в JSONL.
  • Подробности в MARS_MIGRATION.ru.md.

Дата обновления: апрель 2026. Контакт: engineering@okto.ru.