Цифрові двійники без таємниць: Інтернет речей у реальному часі — Частина 3

У попередніх статтях цієї серії ми ознайомилися з концепцією Цифрових двійників та налаштували обліковий запис Realtime Pub/Sub. Тепер настав час взятися до справи і почати моделювати наш перший Цифровий двійник!

pic

Фото від 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. Це гарантує, що пристрій завжди отримує найновіший бажаний стан, ефективно перезаписуючи попередні, ще не застосовані властивості.

Реєстрація пристрою та початкова синхронізація

Ми зосередилися на тому, як сервер керує цифровими двійниками і синхронізує зміни з пристроями. Але як новий пристрій реєструється спочатку і синхронізується зі своїм цифровим двійником? Ось як виглядає цей робочий процес:

  1. Підключення та аутентифікація пристрою:
  • Пристрій підключається до шлюзу 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

Leave a Reply

Your email address will not be published. Required fields are marked *