さくらインターネットが提供する IoT プラットフォームサービス 「sakura.io」を利用して、IoT デバイスから温湿度情報を取得し、FIWARE Orion のエンティティ情報を更新する「デジタルツイン・システム」を構築する方法を紹介します。
前提条件
次の環境が必要となります。
- Raspberry Pi
- sakura.io モジュール (LTE)
- sakura.io HAT for Raspberry Pi
- 環境センサ Enviro for Raspberry Pi (Pimoroni)
- サーバ環境
- docker, git, curl, make コマンド等
Raspberry Pi の種類は、Raspberry Pi WH, 3, 4 等です。sakura.io モジュールと sakura.io HAT for Raspberry Pi は、秋月電子通商や Amazon.co.jp 等から購入できるようです。環境センサの Enviro は、発売元の Pimoroni から購入するか、国内では秋月電子通商、スイッチサイエンス等から購入できるようです。
ソースコード
使用するソースコードは、Github の https://github.com/lets-fiware/lets-fiware.tutorials から入手できます。git コマンドで、リポジトリをクローンして、”sakuraio” ディレクトリに移動してください。
git clone https://github.com/lets-fiware/lets-fiware.tutorials.git
cd ./sakuraio
- IoT デバイス関連のソースコード: ./sakuraio/iot-devcie
- サーバ関連のソースコード: ./sakuraio/ngsi-adapter-websocket
システム構成
センサ・データを FIWARE Orion に送信するため、sakura.io が提供する通信モジュール、通信環境、連携処理を利用します。システムは、通信モジュールを搭載した IoT デバイス、クラウドにある sakura.io プラットフォーム、および、FIWARE プラットフォームで構成されます。FIWARE プラットフォームには、sakura.io から Orion へのデータ送信を仲介する NGSI Adapater for sakura.io があります。
FIWARE IoT Device の作成
ハードウェア
FIWARE IoT Device には、Raspberry Pi を使用します。これに、sakura.io モジュール (LTE), sakura.io HAT for Raspberry Pi および、環境センサ Enviro for Raspberry Pi を接続します。以下の写真は これら部品のRaspberry Pi 4 への搭載例です。
ソフトウェア
Python で記述されたプログラムで、一定間隔でセンサから温湿度、気圧の値を取得し、通信モジュールを制御してデータを sakura.io プラットフォームへ送信します。環境センサおよび通信モジュールの制御には、I2C インタフェースを使用します。ソースコードは、クローンしたリポジトリの “./sakuraio/iot-devcie” にあり、ファイル名は、”sakuraio-i2c.py” です。
プログラム全体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
#!/usr/bin/env python3 # -*- coding: utf-8 -*- import sys import signal import time import sakuraio from sakuraio.hardware.rpi import SakuraIOSMBus from bme280 import BME280 try: from smbus2 import SMBus except ImportError: from smbus import SMBus WAIT = int(os.environ.get('INTERVAL', '60')); def handler(signum, frame): sys.exit(0) def main(): signal.signal(signal.SIGTERM, handler) signal.signal(signal.SIGINT, handler) bus = SMBus(1) bme280 = BME280(i2c_dev=bus) sakuraio = SakuraIOSMBus() try: while (sakuraio.get_connection_status() and 0x80) == 0 : time.sleep(1) print("Connected") while True: sakuraio.enqueue_tx(0, float('{:05.2f}'.format(bme280.get_temperature()))) sakuraio.enqueue_tx(1, float('{:05.2f}'.format(bme280.get_humidity()))) sakuraio.enqueue_tx(2, float('{:05.2f}'.format(bme280.get_pressure()))) sakuraio.send() print("Send Data") time.sleep(WAIT) except Exception as e: print(e) if __name__ == '__main__': main() |
プログラムの実行環境
OS 環境
プログラムは、Raspberry Pi OS または Ubuntu 18.04.4 LTS で実行できます。動作確認した環境は以下の通りです。また、sakura.io モジュールと環境センサの制御に I2C インタフェースを使用しますので、インタフェースを有効化してください。
Raspberry Pi OS (32bit)
1 2 3 4 5 6 7 8 9 |
$ lsb_release -a No LSB modules are available. Distributor ID: Raspbian Description: Raspbian GNU/Linux 10 (buster) Release: 10 Codename: buster $ uname -a Linux pi 4.19.118-v7+ #1311 SMP Mon Apr 27 14:21:24 BST 2020 armv7l GNU/Linux |
Ubuntu 18.04.4 LTS (64bit)
1 2 3 4 5 6 7 8 9 |
$ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 18.04.4 LTS Release: 18.04 Codename: bionic $ uname -a Linux pi 5.3.0-1028-raspi2 #30~18.04.2-Ubuntu SMP Fri Jun 19 05:12:46 UTC 2020 aarch64 aarch64 aarch64 GNU/Linux |
OS 上で実行
プログラムを OS 上でダイレクトに実行する場合、Python ライブラリをインストールします。
1 2 3 |
sudo apt update sudo apt -y install python3 python3-pip python3-smbus sudo pip3 install requests pimoroni-bme280 sakuraio |
関連するライブラリをインストール後に、以下のコマンドでプログラムを起動できます。
sudo python3 sakuraio-i2c.py
Docker コンテナで実行
Docker コンテナ内でプログラムを実行することもできます。コンテナ・イメージの作成は、”make build” で、実行は、”make run” です。詳細は、”./sakuraio/iot-devcie” にある、Dokcerfile と Makefile を参照ください。
コマンド | 説明 |
make build | コンテナ・イメージを作成 |
make run | コンテナを起動 |
make stop | コンテナを提示 |
make rm | コンテナを削除 |
make logs | コンテナのログを表示 |
sakura.io 連携設定
WebSocket 連携サービスの設定
sakura.io のコントロールパネルにログインして、連携サービスの設定を行います。使用する連携サービス名は WebSocket です。任意の名前で WebSocket を作成すると、WebSocket の URL が生成されます。この値を NGSI Adapter for sakura.io (WebSocket) の環境設定で使用します。
サーバ環境の作成
FIWARE プラットフォームは、sakura.io からデータ取得に連携サービスの WebSocket 連携サービスを使用します。WebSocket とは、RFC 6455 で定義された通信プロトコルで、HTTP を使用して双方向通信を行うことができます。NGSI Adapter for sakura.io が WebSocket クライアントとなり、WebSocket サーバの sakura.io にコネクションを張ることで、センサ・データを sakura.ioから受信します。
サーバでは次の3つのコンポーネントを実行します。
- Orion Context Broker : コンテキスト管理
- MongoDB: Orion のバックエンド DB
- NGSI Adapter for sakura.io: WebSocket クライアントとして動作。sakura.io からデータを受信し、Orion のエンティティを更新
Docker 環境
docker-compose ファイル
Orion, MongoDB, NGSI Adapter for sakura.io は、Docker コンテナを利用して、サーバ上にデプロイします。クローンしたリポジトリの “./sakuraio/ngsi-adapter-websocket” に、docker-compose.yml ファイルがあります。SAKURA_IO_WEBSOCKET には、sakura.io 連携設定で生成された WebSocket の URL を設定してください。
環境変数 | 設定値 |
ORION_URL | http://orion:1026/ |
FIWARE_SERVICE | openiot |
FIWARE_SERVICEPATH | /sakuraio |
ENTITY_TYPE | Device |
SAKURA_IO_WEBSOCKET | WebSocket サーバの URL |
FIWARE_DEBUG | true |
Docker コンテナの起動
設定が完了したら、次のコマンドで、Docker コンテナを起動します。これで一連のコンテナが実行されます。初回起動時は、Docker Hub からコンテナ・イメージをプルするため、起動が完了するまで数分かかかります。
docker-compose up -d
コンテナ・イメージのビルド
コンテナ・イメージをビルドしたい場合、make build でビルドできます。詳細は、Makefile を参照ください。
システムの実行
サーバ環境の起動
サーバ環境で、docker-compose を使って一連のコンテナを起動します。 “./sakuraio/ngsi-adapter-websocket” ディレクトリに移動して、次のコマンドを実行します。
docker-compose up -d
次のようなメッセージが表示されます。
1 2 3 4 5 |
$ docker-compose up -d Creating network "ngsi-adapter-websocket_default" with the default driver Creating ngsi-adapter-websocket_mongo_1 ... done Creating ngsi-adapter-websocket_orion_1 ... done Creating ngsi-adapter-websocket_ngsi-adapter_1 ... done |
次に、NGSI Adapter for sakura.io のログを参照して、正常に起動したことを確認します。ログは次のコマンドで表示できます。
docker-compose logs ngsi-adapter
次のようなログを確認できるはずです。
1 2 3 4 5 6 7 8 |
Attaching to ngsi-adapter-websocket_ngsi-adapter_1 ngsi-adapter_1 | NGSI Adapter for sakura.io (WebSocket) ngsi-adapter_1 | WebSocket : wss://api.sakura.io/ws/v1/00000000-0000-0000-0000-000000000000 ngsi-adapter_1 | Orion URL : http://orion:1026/, EntityType: Device ngsi-adapter_1 | FWIARE-Service: openiot, FIWARE-ServicePath: /sakuraio, debug: true ngsi-adapter_1 | Connected ngsi-adapter_1 | {"type":"keepalive","datetime":"2020-07-18T01:00:56.119847397Z"} ngsi-adapter_1 | keepalive: 2020-07-18T01:00:56.119847397Z |
IoT デバイスの起動
Raspberry Pi で “./sakuraio/iot-devcie” ディレクトリに移動して Python のプログラムを起動します。
OS上で実行
次のコマンドを実行すると、プログラムが起動して、コンソールにメッセージが表示されます。
sudo python3 sakuraio-i2c.py
Docker コンテナで実行
次のコマンドを実行して、コンテナを起動します。
make run
メッセージは次のコマンドで表示できます。
make logs
実行結果
次のようなメッセージが表示されます。”Connected” は、IoT デバイスが sakura.io に接続したことを示し、”Send Data” は、センサ・データを sakura.io に送信したことを示します。プログラムは60秒毎にセンサ・データを送信します。そのたびに、”Send Data” のメッセージが表示されます。
1 2 3 4 |
Connected Send Data Send Data Send Data |
NGSI Adapter for sakura.io の動作確認
NGSI Adapter for sakura.io のログを参照すると、sakura.io からセンサ・データを受信し、Orion のエンティティ情報を更新したことを確認できます。サーバ環境で、”./sakuraio/ngsi-adapter-websocket” ディレクトリに移動し、”docker-compose logs ngsi-adapter” コマンドを実行します。次のようなメッセージを確認できるはずです。
1 2 3 4 5 6 7 8 |
{"module":"xCr8vqsJ0Zbe","type":"channels","datetime":"2020-07-18T00:57:12.208714405Z","payload":{"channels":[{"channel":0,"type":"d","value":28.15,"datetime":"2020-07-18T00:57:12.132715578Z"},{"channel":1,"type":"d","value":56.14,"datetime":"2020-07-18T00:57:12.158715578Z"},{"channel":2,"type":"d","value":1006.09,"datetime":"2020-07-18T00:57:12.184715578Z"}]}} {"temperature":{"type":"Number","value":28.15,"metadata":{"TimeInstant":{"type":"DateTime","value":"2020-07-18T00:57:12.132Z"}}},"relativeHumidity":{"type":"Number","value":56.14,"metadata":{"TimeInstant":{"type":"DateTime","value":"2020-07-18T00:57:12.158Z"}}},"atmosphericPressure":{"type":"Number","value":1006.09,"metadata":{"TimeInstant":{"type":"DateTime","value":"2020-07-18T00:57:12.184Z"}}},"id":"urn:ngsi-ld:Device:xCr8vqsJ0Zbe","type":"Device","modifiedAt":{"type":"DateTime","value":"2020-07-18T00:57:12.208Z"}} xCr8vqsJ0Zbe {"type":"keepalive","datetime":"2020-07-18T00:57:19.059711727Z"} keepalive: 2020-07-18T00:57:19.059711727Z {"module":"xCr8vqsJ0Zbe","type":"channels","datetime":"2020-07-18T00:57:27.327071791Z","payload":{"channels":[{"channel":0,"type":"d","value":28.29,"datetime":"2020-07-18T00:57:27.250072779Z"},{"channel":1,"type":"d","value":55.65,"datetime":"2020-07-18T00:57:27.276072779Z"},{"channel":2,"type":"d","value":1006.09,"datetime":"2020-07-18T00:57:27.303072779Z"}]}} {"temperature":{"type":"Number","value":28.29,"metadata":{"TimeInstant":{"type":"DateTime","value":"2020-07-18T00:57:27.250Z"}}},"relativeHumidity":{"type":"Number","value":55.65,"metadata":{"TimeInstant":{"type":"DateTime","value":"2020-07-18T00:57:27.276Z"}}},"atmosphericPressure":{"type":"Number","value":1006.09,"metadata":{"TimeInstant":{"type":"DateTime","value":"2020-07-18T00:57:27.303Z"}}},"id":"urn:ngsi-ld:Device:xCr8vqsJ0Zbe","type":"Device","modifiedAt":{"type":"DateTime","value":"2020-07-18T00:57:27.327Z"}} xCr8vqsJ0Zbe |
エンティティの確認
サーバ環境で、Orion にアクセスしてエンティティ情報を確認してみます。次のコマンドで、エンティティ情報を取得できます。
curl -sS -H ‘FIWARE-Service: openiot’ -H ‘FIWARE-ServicePath: /sakuraio’ localhost:1026/v2/entities
実行結果
実行結果は次の通りです。このような情報を取得できれば、エンティティ情報を正しく更新できています。エンティティの id 属性値は、”urn:ngsi-ld:Device:xCr8vqsJ0Zbe” で、末尾の “xCr8vqsJ0Zbe” は通信モジュールの ID です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
$ curl -sS -H 'FIWARE-Service: openiot' -H 'FIWARE-ServicePath: /sakuraio' \ localhost:1026/v2/entities | jq . [ { "id": "urn:ngsi-ld:Device:xCr8vqsJ0Zbe", "type": "Device", "atmosphericPressure": { "type": "Number", "value": 1006.11, "metadata": { "TimeInstant": { "type": "DateTime", "value": "2020-07-18T00:58:42.00Z" } } }, "modifiedAt": { "type": "DateTime", "value": "2020-07-18T00:58:42.00Z", "metadata": {} }, "relativeHumidity": { "type": "Number", "value": 55.51, "metadata": { "TimeInstant": { "type": "DateTime", "value": "2020-07-18T00:58:42.00Z" } } }, "temperature": { "type": "Number", "value": 28.38, "metadata": { "TimeInstant": { "type": "DateTime", "value": "2020-07-18T00:58:42.00Z" } } } } ] |
NGSI Adapter for sakura.io の解説
NGSI Adapter for sakura.io は、sakura.io から送信されたデータを受信し、NGSI データモデル形式のエンティティ情報を作成して、Orion に更新リクエストを行います。
プログラムは Node.js で記述された WebSocket クライアントです。メイン処理は、server.js にあり、ソースコードは次の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
/* * NGSI Adapter for sakura.io (WebSocket) * https://github.com/lets-fiware/lets-fiware.tutorials * * Copyright (c) 2020 Kazuhito Suda * Licensed under the MIT license. */ 'use strict'; const config = require('./config'); const axios = require('axios'); const WebSocket = require('ws'); const debug = config.debug; const secret = config.secret; const orionUrl = config.orionUrl; const entityType = config.entityType; const websocketUrl = config.websocket; process.on('SIGTERM', () => { console.log('Got SIGTERM'); process.exit(0); }); console.log('NGSI Adapter for sakura.io (WebSocket)'); console.log(`WebSocket : ${websocketUrl}`); console.log(`Orion URL : ${orionUrl}, EntityType: ${entityType}`); console.log(`FWIARE-Service: ${config.fiwareService}, FIWARE-ServicePath: ${config.fiwareServicepath}, debug: ${config.debug}`); const ws = new WebSocket(websocketUrl); ws.on('open', function () { console.log('Connected'); }); ws.on('close', function () { console.log('Disconected'); }); ws.on('message', function (data) { try { if (debug) { console.log(data); } data = JSON.parse(data); module = data.module; const payload = data.payload; let entity = {}; switch (data.type) { case 'channels': for (let i = 0; i < payload.channels.length; i++) { const channel = payload.channels[i]; entity[mapper[i].name] = mapper[i].func(channel); } break; case 'location': if (payload.coordinate != null) { entity['location'] = attrLocation(payload.coordinate); } break; case 'connection': console.log(`connection: ${data.datetime} : is_online : ${payload.is_online}`); break; case 'keepalive': if (debug) { console.log(`keepalive: ${data.datetime}`); } break; } if (!!Object.keys(entity).length) { entity['id'] = `urn:ngsi-ld:${entityType}:${data.module}`; entity['type'] = entityType; entity['modifiedAt'] = { type: "DateTime", value: new Date(data.datetime) }; opUpdate(entity); } } catch (error) { console.log(`Exception: ${error.message}`); } console.log(`${module}`); }); const attrNumber = function attrNumber(channel) { return { type: 'Number', value: channel.value, metadata: { TimeInstant: { type: "DateTime", value: new Date(channel.datetime) } } }; } const attrLocation = function attrLocation(coord) { return { type: 'geo:json', value: { type: 'Point', coordinates: [ coord.longitude, coord.latitude ] }, metadata: { range: { type: "Number", value: coord.range_m } } }; } const mapper = [ { name: 'temperature', func: attrNumber }, { name: 'relativeHumidity', func: attrNumber }, { name: 'atmosphericPressure', func: attrNumber }, ]; const headers = { 'FIWARE-Service': config.fiwareService.trim().toLowerCase(), 'FIWARE-ServicePath': config.fiwareServicepath.trim() }; function opUpdate(entity) { const url = new URL(`/v2/op/update`, orionUrl); if (debug) { console.log(JSON.stringify(entity)); } axios({ method: 'post', url: url.href, headers: headers, data: { actionType: 'append', entities: [entity] } }) .then(response => { if (response.status != 204) { console.log(`opUpdate : ${error.response.status}`); } }) .catch(error => { console.log(`opUpdate : ${error.response.status}`); }) } |
データ形式の変換
sakura.io から送信されるデータは、channels 形式と呼ばれる JSON データです。これを NGSIデータモデル形式の JSON データに変換します。
sakura.io channels 形式の JSON データ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
{ "module": "xCr8vqsJ0Zbe", "type": "channels", "datetime": "2020-07-14T08:35:23.42296446Z", "payload": { "channels": [ { "channel": 0, "type": "d", "value": 25.47, "datetime": "2020-07-14T08:35:23.345965388Z" }, { "channel": 1, "type": "d", "value": 92.09, "datetime": "2020-07-14T08:35:23.372965388Z" }, { "channel": 2, "type": "d", "value": 661.89, "datetime": "2020-07-14T08:35:23.398965388Z" } ] } } |
FIWARE NGSI データモデル形式の JSON データ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
{ "temperature": { "type": "Number", "value": 25.47, "metadata": { "TimeInstant": { "type": "DateTime", "value": "2020-07-14T08:35:23.345Z" } } }, "relativeHumidity": { "type": "Number", "value": 92.09, "metadata": { "TimeInstant": { "type": "DateTime", "value": "2020-07-14T08:35:23.372Z" } } }, "atmosphericPressure": { "type": "Number", "value": 661.89, "metadata": { "TimeInstant": { "type": "DateTime", "value": "2020-07-14T08:35:23.398Z" } } }, "id": "urn:ngsi-ld:Device:xCr8vqsJ0Zbe", "type": "Device", "modifiedAt": { "type": "DateTime", "value": "2020-07-14T08:35:23.422Z" } } |
メタデータ TimeInstant
温湿度の属性値は、TimeInstant というメタデータを持ち、sakura.io から送信されたデータ中の時刻を保持しています。この TimeInstant は、この属性値を履歴データして保存するときに特別な意味を持ちます。
FIWARE では、コンテキスト情報 (属性値) の更新履歴を永続化する場合、Cygnus という Generic Enabler を使用します。Cygnus は、通常、永続化する属性値を受信した時刻をタイムスタンプとして保存します。しかし、NGSISTHSink や NGSIMongoSink を利用しているケースで、 属性が TimeInstant メタデータを持っている場合、受信時刻の代わりに TimeInstant の値をタイムスタンプとして使用します。
この TimeInstant を使用することで、sakura.io から受信したデータ中のセンサ値と時刻を永続化できます。
エンティティ情報の更新 /v2/op/update
NGSI v2 API は、エンティティを管理するための RESTful なオペレーションとは別に、バッチ・オペレーションを持っています。バッチ更新オペレーション (POST /v2/op/update) とバッチ・クエリ・オペレーション (POST /v2/op/query) です。バッチ更新オペレーションは、更新アクション・タイプ (Update action types) と呼ばれる、いつかの動作モードを持っています。
このプログラムでは、エンティティの管理にバッチ更新オペレーション (POST /v2/op/update) の append 更新アクション・タイプを使用しています。このアクション・タイプは、エンティティの作成、エンティティの属性の作成、およびエンティティの属性の更新を行うことができます。
エンティティが存在しない場合、POST /v2/entities と同等の処理を行い、エンティティが存在する場合、 POST /v2/entities/<id>/attrs と同等の処理を行います。