У попередніх статтях цієї серії ми ознайомилися з концепцією Цифрових двійників та налаштували обліковий запис Realtime Pub/Sub. Тепер настав час взятися до справи і почати моделювати наш перший Цифровий двійник!
Фото від Jakub Zerdzicki: https://www.pexels.com/photo/camera-and-electric-bulbs-on-colorful-background-19701594/
Визначення “Речі”: основа вашого Цифрового двійника
В основі кожного Цифрового двійника лежить “Річ” — цифрове відображення фізичного чи віртуального активу. Уявіть собі це як креслення для вашого пристрою, системи чи сутності в екосистемі IoT.
Натхненні детальною документацією від Bosch IoT Things, ми прийняли гнучку та потужну модель для визначення наших Речей.
Наше визначення Речі, задане за допомогою JSON Schema, надає структурований спосіб фіксувати всі основні аспекти активу:
{
"type": "object",
"title": "Thing",
"description": "Універсальна сутність, що представляє фізичний або віртуальний актив в додатку IoT.",
"properties": {
"thingId": {
"type": "string",
"description": "Унікальний ідентифікатор для Речі, у форматі 'namespace:thing-name'",
"pattern": "^[a-zA-Z0-9\\.\\-_]+:[a-zA-Z0-9\\.\\-_%]+$",
"maxLength": 256,
"examples": [
"com.example.iot-course:device-001"
]
},
"namespace": {
"type": "string",
"description": "Простір імен для Речі, що слідує нотанції зворотного доменного імені",
"pattern": "^[a-zA-Z][a-zA-Z0-9\\.]*[a-zA-Z0-9]$",
"examples": [
"com.example.iot-course"
]
},
"definition": {
"type": "string",
"description": "Необов'язковий ідентифікатор, що посилається на зовнішню модель або тип для Речі (наприклад, namespace:name:version)",
"pattern": "^[a-zA-Z0--_]+:[a-zA-Z0-9\\.\\-_]+$",
"examples": [
"com.example:thermostat:1.0.0"
]
},
"attributes": {
"type": "object",
"description": "Статичні, описові властивості Речі",
"additionalProperties": true,
"examples": [
{
"manufacturer": "Example Corp",
"model": "Smart Thermostat X",
"location": "Living Room"
}
]
},
"features": {
"type": "object",
"description": "Логічні групи пов'язаних даних та функціональностей",
"additionalProperties": {
"type": "object",
"properties": {
"definition": {
"type": "array",
"description": "Необов'язковий масив ідентифікаторів, що посилаються на зовнішні моделі для функції",
"items": {
"type": "string",
"pattern": "^[a-zA-Z0-9\\.\\-_]+:[a-zA-Z0-9\\.\\-_]+:[a-zA-Z0-9\\.\\-_]+$"
},
"examples": [
["com.example:temperaturesensor:1.0.0"]
]
},
"properties": {
"type": "object",
"description": "Поточний стан функції, як його повідомляє пристрій",
"additionalProperties": true,
"examples": [
{
"temperature": 22.5,
"status": "OK"
}
]
},
"desiredProperties": {
"type": "object",
"description": "Бажаний стан функції, який має бути встановлений на пристрої",
"additionalProperties": true,
"examples": [
{
"temperature": 25,
"fanSpeed": "high"
}
]
},
"lastAppliedUpdate": {
"type": "string",
"description": "ULID, що представляє останнє застосоване оновлення для цієї функції",
"pattern": "^[0-9A-Z]{26}$",
"examples": [
"01H47J5YS3GZ0EK69G5ZS327AV"
]
},
"lastReceivedUpdate": {
"type": "string",
"description": "ULID, що представляє останнє отримане оновлення для цієї функції (може ще не бути застосованим)",
"pattern": "^[0-9A-Z]{26}$",
"examples": [
"01H47J60F58S48Q49889K838SQ"
]
}
},
"required": [
"properties"
]
}
}
},
"required": [
"thingId",
"namespace",
"features"
]
}
ULID = Універсально унікальні лексикографічно сортувальні ідентифікатори (https://github.com/ulid/spec)
Основні компоненти Речі:
thingId
: Унікальний ідентифікатор для вашої Речі. Він слідує простому форматуnamespace:thing-name
. Наприклад,com.example.iot-course:lightbulb-001
унікально ідентифікує нашу розумну лампочку.namespace
: Використовується для організації і запобігання конфліктам імен, простори імен слідують зворотній нотанції доменних імен (наприклад,com.example.iot-course
).attributes
: Це описові властивості Речі, що не змінюються часто. Приклади включаютьmanufacturer
,model
, таlocation
.features
: Тут визначається основна функціональність та дані вашої Речі. Функції є логічними групами пов'язаних властивостей.
Наприклад, розумний термостат може мати функції, такі як "TemperatureSensor" (Датчик температури) та "HeaterControl" (Управління обігрівачем).features/properties
: Відображають поточний стан функції, як його повідомляє фізичний пристрій.features/desiredProperties
: Описують стан, який ви хочете встановити на пристрої. Система Digital Twin (Цифровий двійник) відповідає за передачу цих бажаних змін на фізичний пристрій.features/lastAppliedUpdate
таfeatures/lastReceivedUpdate
: Ці поля, що використовують ULID (Універсально унікальні лексикографічно сортувальні ідентифікатори), допомагають нам відслідковувати процес синхронізації між Цифровим двійником та фізичним пристроєм, забезпечуючи надійні оновлення.
Примітка: Глибша валідація та вкладені схеми були опущені для спрощення.
Приклад: Моделювання розумної лампочки
Давайте проілюструємо це на конкретному прикладі. Ми змоделюємо розумну лампочку як Цифровий двійник, використовуючи наше визначення Речі:
{
"thingId": "com.example.iot-course:lightbulb-001",
"namespace": "com.example.iot-course",
"attributes": {
"manufacturer": "Example Corp",
"model": "smart-lightbulb-01",
"location": "Kitchen"
},
"features": {
"SmartLight": {
"properties": {
"isOn": false,
"brightness": 0,
"color": "white"
},
"lastAppliedUpdate": "01H47K5Y79GZ0EK69G5ZS328BC",
"lastReceivedUpdate": "01H47K5Y79GZ0EK69G5ZS328BC",
"desiredProperties": {
"isOn": true,
"brightness": 100,
"color": "blue"
}
}
}
}
У цьому прикладі:
- У нас є
thingId
=com.example.iot-course:lightbulb-001
. - Властивості
attributes
описують нашу лампочку: Вона виготовлена компанією "Example Corp", модель "smart-bulb-01" та знаходиться в "Кухні". - Ми визначили одну функцію під назвою
SmartLight
, яка об'єднує всі функціональності лампочки. Зверніть увагу, що в реальних сценаріях очікується кілька функцій. - Наразі лампочка вимкнена (
isOn: false
), яскравість дорівнює 0, а колір — "білий" (як вказано вproperties
). - Однак ми маємо налаштування
desiredProperties
: Ми хочемо включити лампочку (isOn: true
), встановити яскравість на 100 і змінити колір на "синій".
Натхнення від Bosch IoT Things
Варто зазначити, що наше визначення Речі черпає натхнення з чіткої та структурованої моделі, описаної в документації Bosch IoT Things. Їхня модель забезпечує надійний фундамент для управління складними IoT пристроями та системами, і ми адаптували її для створення зрозумілої та доступної моделі для нашого курсу Digital Twin.
Моделювання бекенду
У попередньому розділі ми визначили структуру нашої "Речі" — цифрового представлення наших IoT активів. Тепер перейдемо до моделювання інтерфейсів бекенду, які підтримуватимуть взаємодії нашого Цифрового двійника, зосереджуючись на надійному підході до синхронізації за допомогою списку завдань.
Ми будемо використовувати TypeScript для визначення цих інтерфейсів, надаючи чіткий і типобезпечний план для нашої реалізації.
Визначення основних інтерфейсів
Для керування нашими Цифровими двійниками ми визначимо такі основні інтерфейси:
Thing
: Описує структуру Речі (як було визначено в попередньому розділі).Feature
: Описує логічне групування властивостей та стану всередині Речі (як було визначено в попередньому розділі).Task
: Описує операцію, яку потрібно виконати на віддаленому пристрої.ThingModel
: Окреслює методи для взаємодії з нашим сховищем даних для виконання CRUD операцій з Речами, Функціями та Завданнями.ThingService
: Визначає методи для обробки бізнес-логіки, пов'язаної з нашими Цифровими двійниками, використовуючиThingModel
для взаємодії з даними та керування списком завдань для синхронізації.
Інтерфейси Thing
, Feature
та Task
Розглянемо знову інтерфейси Thing
та Feature
, а також додамо новий інтерфейс Task
:
interface Thing {
thingId: string;
namespace: string;
definition?: string;
attributes?: Record;
features: Record;
}
interface Feature {
definition?: string[];
properties: Record;
desiredProperties?: Record;
lastAppliedUpdate?: string; // ULID
lastReceivedUpdate?: string; // ULID
}
interface Task {
taskId: string; // ULID
operation: 'UPDATE_PROPERTIES'; // Для спрощення ми будемо мати лише цей тип операції
thingId: string;
featureId: string;
payload: Record; // Дані, пов'язані з операцією (наприклад, desiredProperties)
status: 'pending' | 'processing' | 'completed' | 'failed';
timestamp: string; // ISO 8601 timestamp
lastError?: string; // Інформація про помилку, якщо вона є
}
Інтерфейс ThingModel
Інтерфейс ThingModel
вказує, як ми будемо взаємодіяти з базою даних для керування нашими Речами, Функціями та Завданнями:
interface ThingModel {
// Методи, пов'язані з Речами:
createThing(thing: Thing): Promise;
getThing(thingId: string): Promise;
updateThingAttributes(thingId: string, attributes: Record): Promise;
getFeature(thingId: string, featureId: string): Promise;
updateFeatureProperties(thingId: string, featureId: string, properties: Record, lastAppliedUpdate: string): Promise;
// Методи, пов'язані з Завданнями:
addTask(task: Task): Promise;
getPendingTasks(thingId: string): Promise;
updateTaskStatus(taskId: string, status: 'processing' | 'completed' | 'failed', error?: string | undefined): Promise;
getTask(taskId: string): Promise;
deleteTask(taskId: string): Promise;
getPendingTask(thingId: string, featureId: string): Promise;
updateTaskPayload(taskId: string, payload: Record): Promise;
}
Інтерфейс ThingModel
також включає методи для керування завданнями (механізм синхронізації пристроїв):
addTask
: Додає нове завдання до списку завдань.getPendingTasks
: Отримує всі очікуючі завдання для заданогоthingId
.updateTaskStatus
: Оновлює статус завдання (наприклад, наprocessing
,completed
абоfailed
).getTask
: Отримує конкретне завдання за його ID (taskId
).deleteTask
: Видаляє завдання з бази даних.getPendingTask
: Отримує очікуюче завдання заthingId
таfeatureId
.updateTaskPayload
: Оновлює дані завдання.
Інтерфейс ThingService
Інтерфейс ThingService
визначає бізнес-логічні методи для роботи з Цифровими двійниками, включаючи важливу частину — керування завданнями для синхронізації:
interface ThingService {
createThing(partialThing: Partial): Promise;
getThing(thingId: string): Promise;
updateThingAttributes(thingId: string, attributes: Record): Promise;
getFeature(thingId: string, featureId: string): Promise;
setDesiredFeatureProperties(thingId: string, featureId: string, desiredProperties: Record): Promise;
processDeviceConfirmation(thingId: string, featureId: string, updatedFeature: Feature, lastAppliedUpdate: string, status: 'completed' | 'failed', error?: string): Promise;
}
Синхронізація пристроїв за допомогою завдань
Ось як буде працювати процес синхронізації між пристроями та сервером за допомогою впорядкованого списку завдань:
1.
Встановлення бажаних властивостей:
- Коли викликається
setDesiredFeatureProperties
, сервіс перевіряє, чи є незавершене завданняUPDATE_PROPERTIES
для заданогоthingId
іfeatureId
. - Якщо таке завдання є, його
payload.desiredProperties
оновлюється новими значеннями. Також оновлюється мітка часу завдання. - Якщо незавершеного завдання немає, створюється нове завдання
Task
з операцієюUPDATE_PROPERTIES
, яке додається до бази даних зі статусомpending
. Feature.lastReceivedUpdate
оновлюється ID нового завдання.
2. Застосування змін на пристрої та підтвердження (з обробкою помилок):
- Пристрій отримує завдання та змінює його статус на
processing
. - Пристрій намагається застосувати зміни з
payload.desiredProperties
до свого стану. - Якщо зміни були успішно застосовані: Пристрій надсилає повідомлення успіху шлюзу, в якому міститься:
taskId
, оновлений об'єктFeature
(з новимиproperties
) іlastAppliedUpdate
(що відповідає ID завдання). - Якщо під час застосування виникла помилка: Пристрій надсилає повідомлення неуспіху шлюзу, в якому міститься:
taskId
, повідомлення про помилку або код уlastError
, а також, за потреби, частково оновлений об'єктFeature
(якщо це застосовно).
3. Оновлення на сервері (з обробкою помилок):
- Шлюз отримує підтвердження (успішне чи неуспішне).
- Шлюз викликає
processDeviceConfirmation
уThingService
. processDeviceConfirmation
отримуєThing
,Feature
іTask
за допомогоюtaskId
.processDeviceConfirmation
перевіряє, чи знаходиться завдання у станіprocessing
.- Якщо підтвердження вказує на успіх:
processDeviceConfirmation
оновлюєproperties
вFeature
на основі підтвердження, встановлюєlastAppliedUpdate
як ID завдання, видаляєdesiredProperties
, позначає завдання якcompleted
і видаляє його. - Якщо підтвердження вказує на помилку:
processDeviceConfirmation
за бажанням оновлюєproperties
вFeature
з будь-якими частково застосованими змінами з повідомлення про підтвердження, не оновлюєlastAppliedUpdate
, позначає завдання якfailed
, заповнюєlastError
повідомленням про помилку з пристрою. Завдання з помилкою залишатиметься в базі для подальшого аналізу або повторної спроби.
Обробка потенційних конфліктів
Ми свідомо спрощуємо вирішення конфліктів для потреб цього курсу. Коли здійснюється новий виклик setDesiredFeatureProperties
, а завдання все ще очікує:
ThingService
знаходить існуюче незавершене завданняUPDATE_PROPERTIES
для того жthingId
іfeatureId
.ThingService
оновлюєpayload.desiredProperties
цього незавершеного завдання новимиdesiredProperties
. Це гарантує, що пристрій завжди отримує найновіший бажаний стан, ефективно перезаписуючи попередні, ще не застосовані властивості.
Реєстрація пристрою та початкова синхронізація
Ми зосередилися на тому, як сервер керує цифровими двійниками і синхронізує зміни з пристроями. Але як новий пристрій реєструється спочатку і синхронізується зі своїм цифровим двійником? Ось як виглядає цей робочий процес:
- Підключення та аутентифікація пристрою:
- Пристрій підключається до шлюзу Realtime Pub/Sub і проходить аутентифікацію. Це гарантує, що тільки авторизовані пристрої можуть взаємодіяти зі своїми цифровими двійниками.
2. Надсилання повідомлення device-sync
:
- Після підключення та аутентифікації пристрій надсилає повідомлення
device-sync
на сервер через шлюз Realtime Pub/Sub. - Це повідомлення публікується на певній темі, на яку підписується сервер (так званій:
secure/inbound
). - Тіло повідомлення
device-sync
містить наступне:
{
"thingId": "com.example.iot-course:lightbulb-001",
"namespace": "com.example.iot-course",
"features": {
"SmartLight": {
"properties": {
"isOn": false,
"brightness": 0,
"color": "white"
},
"lastAppliedUpdate": null
}
}
}
3.
**Створення/Оновлення Thing на сервері:
- Сервер отримує повідомлення
device-sync
, і спеціальний обробник (handler) обробляє це повідомлення. - Якщо Thing з заданим
thingId
не існує: Сервер створює новийThing
в базі даних, використовуючи інформацію з повідомленняdevice-sync
. - Якщо Thing з заданим
thingId
вже існує: Сервер оновлює існуючийThing
відповідно до стану, наданого в повідомленніdevice-sync
, забезпечуючи, щоб цифровий двійник відображав початковий стан пристрою.lastAppliedUpdate
для кожної функції встановлюється наnull
.
4. Початок синхронізації:
- Після обробки повідомлення
device-sync
, починається стандартний процес синхронізації. - Сервер перевіряє наявність незавершених завдань для пристрою. Існуючі завдання надсилаються на "пристрій" через приватну тему Realtime Pub/Sub.
Переваги цього підходу
- Безстейтовий сервер: Серверу не потрібно зберігати постійний стан щодо підключених пристроїв.
- Ідемпотентність: Повідомлення
device-sync
можна надсилати кілька разів без виникнення проблем. - Автономна реєстрація: Пристрої можуть реєструвати себе динамічно.
Що далі?
У наступній статті ми реалізуємо ці інтерфейси в реальному сервері та покажемо, як підключити наші цифрові двійники до живих даних за допомогою сервісу Realtime Pub/Sub.
Залишайтеся з нами!
Перекладено з: Digital Twins Demystified: Realtime IoT in Action — Part 3