Архитектура системы OKTO¶
Подробное техническое описание всех компонентов платформы прослеживаемости OKTO: edge-сервис, центральный локальный сервер, центральная консоль, интеграция с облаком OKTO.
Аудитория: инженеры, DevOps, архитекторы. Уровень детализации: средний-глубокий — конкретные классы, таблицы, пакеты, очереди. Связанные документы: SERVER_MANAGEMENT.ru.md, API_REFERENCE.ru.md, DEPLOYMENT.ru.md, DEVELOPER_GUIDE.ru.md.
Содержание¶
- Обзор
- Топология развёртывания
- Состав компонентов
- Технологический стек
- Структура репозитория
- Ключевые сервисы edge-сервиса
- Ключевые сервисы центрального сервера
- Модель данных
- Поток данных: производство → облако
- Режимы подключения
- Управление устройствами (WebSocket)
- Безопасность и аутентификация
- Параллелизм и потоки
- Наблюдаемость
- Фронтенд-архитектура
- Ограничения и известные компромиссы
- 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:
- Hoplite читает YAML + env.
- Запускается Ktor Netty на
server.port. - Koin-модуль собирает граф зависимостей.
- После старта
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]
Администратор может:
- Задать глобальный дефолт (UI → «Режим подключения»).
- Включить/выключить device override (флаг
allow_device_mode_override). - Переопределить режим для конкретного устройства (кнопка в карточке устройства).
Edge-сервис читает режим при старте из конфига, но при ConnectionMode change с сервера (через WS) переключает клиенты «на лету»:
DIRECT_CLOUD→ServerConnectionService.stop()+OfflineQueueService.useCloudClient().VIA_LOCAL_SERVER→ServerConnectionService.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,ShutdownOsCmdForceSyncCmd,ClearQueueCmdPullLogsCmd { 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}, TTLauth.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.inbound—MutableSharedFlow<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:
useLiveEvents→openWs('/ws/dashboard?token=…')→ reconnect loop + subscription-сообщение при open. - Auth:
AuthContextхранит user + token. Token сохраняется вlocalStorage(okto.jwt). При 401 → clearAuth + redirect/login.
i18n¶
- По умолчанию русский (
lng: 'ru',fallbackLng: 'ru'). - Переключение через
Preferences→setLanguage('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-motionaware.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на edge —push_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.