はじめに
WebSensorServer Softwareのkikoriについて説明します。kikoriはGroovy-IoT用に開発されました。このkikoriの使いこなしについてenebular-editorを用いて、様々なフローを提示して説明したいと思います。
kikoriの起動
$ java -cp modules:kikori-1.5.0-SNAPSHOT.jar kikori.shell system.clj
system.clj
kikoriの動作は設定ファイル(system.clj)に設定されています。特に変わった設定でなければkikoriの標準パッケージに含まれる system.cljをそのまま使うのが良いでしょう。今回の説明では標準添付のsystem.cljを用いての解説になります。最後にsystem.cljの構成について解説します。
Groovy-IoT&kikori対応センサ
Groovy-IoTに接続できるセンサの種別として、
- I2C, UART
I2C, UARTはこれら自身がステートマシンであるのである程度のプログラミングが必要になります。これらはモジュールとしてkikori本体に組み込まれていたり、Javaのプラグインとして実装されています。現在、kikoriでサポートされていないセンサであってもプラグイン・モジュールの実装により対応可能です。
Connector | Sensor/Actuator | Model-No. | Note |
---|---|---|---|
I2C | Temp, RH, Pressure | Grove-Barometer Sensor(BME280) |
|MEMS thermal sensor|[D6T-44L by OMRON](https://www.omron.co.jp/ecb/product-detail?partId=5747)| |
|Vibration sensor |[D7S by OMRON](https://www.omron.co.jp/ecb/product-detail?partNumber=D7S) | |
|Accelerometer |[ADXL345 by Grove](https://www.switch-science.com/catalog/972/) | |
UART |GPS |Grove-GPS| |
- GPIO, ADC
GPIOとADCに関しては、基本的にはセンサ素子やスイッチ/LEDを接続します。これは直接、素子から読み/書きを行うのでI2Cのようなプラグイン・モジュールは必要ありません。
その他のデバイス
kikoriで対応しているのは一般的なセンサ/アクチュエータだけではありません。IoTを便利にするデバイスにも対応しています。
LCD
現在はこちらのRaspberryPi用のLCDもkikoriで対応しています。開発側のPCではWeb-Browserを仮想LCDとして利用します。LCDはRaspberryPi専用デバイスとなりますので、RaspberryPiを使っている方で必要な場合は検討されと良いでしょう。詳しくは以下を参照して下さい。
irMagician
irMagicianは家電制御のための赤外線リモコンアダプタになります。センサ情報を元に家電制御が可能になります。
Camera
PCに内蔵しているカメラやUSBで接続するカメラやRasPiのカメラなどに対応します。
Omron 環境センサ (2JCIE-BU)
このセンサはUSB/BTのコネクティビティを持ちますが、USBのみが利用可能です。
センサ情報の取得方法
kikoriへのセンサやアクチュエータのアクセス方法は大きく分けて、WebAPI, WebSocket があります。ここではそれぞれどの様に行うかを具体的に見ていきます。
WebAPI
一般的なアクセス方法です。サーバに対して非同期でリクエストを送り、サーバからのレスポンスを待ちます。リクエストの中にセンサの種別やチャンネルなどを設定します。一般的なリクエストとレスポンスは下記の通りになります。
- センサ情報を取得してみよう
この特性から、HTTPへの接続に一般的なコマンドcurl
を使ってセンサデータを得ることができます。上記のセンサの接続で以下のコマンドを投入します。
curl "http://localhost:3000/sensor?ids=BME1"
{"BME1":{"pressure":1018.9275504721079,"temperature":18.511598834913457,"humidity":40.085342199695326}}
Request&Response
Node-REDのNodeではHTTP-Requestを使います。一般的な構成は以下の通りになります。
HTTP-Request ノードにセンサを含めたURLを記述して、kikoriに投げるとBME280のセンサ情報の結果が応答されるのがわかると思います(Debugノード参照)
Injection ノードの「繰り返し」を設定すれば、周期的にセンサ情報を取得することも可能になります。
WebSocket
もう一つのデータの取得方法として、WebSocket があります。より速く周期的にデータを取得したいときにInjection-Nodeの代わりに利用できます。Injection-Nodeでは1sec単位の設定ですがkikoriを用いてのWebSocketだと、0.1sec単位での利用が可能です(最小分解能については利用するシステムに依存します)。
WebSocketを利用するためにはいくつか設定が必要になります。それらを順番に見ていきましょう。
Start
Node-REDの起動1秒後に設定されます(初期設定)
Stop
WebSocketはIntervalを設定するとその周期で連続的に動作します。その動作を止めたいときに使います。
Interval-Set
WebSocketの動作間隔(Interval)を設定します。起動2秒後(初期設定終了後)にセットされます。
HTTP-Request内のURLには下記の通りに記述します。
http://localhost:3000/write?target=TICK0&interval=100
kikoriの機能と応用例
kikoriでサポートしているデバイスについて解説します。
Groovy-IoT
Groovy-IoTは以下の機能を持ち、kikoriから制御されます。
GPIO
GPIOは入力と出力を持ちます。スイッチ(ボタン)の読み取りやLEDの点滅(Lチカ)に利用できます。以下にLチカの方法について解説します。
- LEDの点灯
curl "http://localhost:3000/write?target=GP0&value=1"
{"result":"success","target":"GP0","value":"1"}
- LEDの消灯
curl "http://localhost:3000/write?target=GP0&value=0"
{"result":"success","target":"GP0","value":"0"}
-
LEDの点滅(Lチカ)
LEDの点滅には先に述べたInjection-Node
を使う方法とWebSocket
を用いた2つの方法があります。これらの方法を下記に示します。 -
LED(GPIO)のOn/Off
右端のLED-On/Off にはそれぞれ数のようにHTTP-Requestを設定します。
ADC
ADCを利用する場合は、いくつかのパラメータを設定する必要があります。一部、設定ファイルに記述するものやURIで動的に設定できるものがあります。
-
使用チャンネル
ADCはADC1, ADC2, ADC3と合計3ch使えます。どのチャネルを割り当てるかをsystem.cljに記述します。 -
量子化ビット数
量子化ビット数(電源範囲を何bitで分割するか)は本デバイスの場合は10bit固定になっています。つまり、2^10=2048の分解能という事になります。 -
リファレンス電圧
ADCを行うときの基準電圧の設定をします。1.024, 2.048, 4.096@VDD=5V設定時, VDDとなります。この設定により擬似的なアンプとして利用できます。 -
サンプリング
ADCのサンプリング方法は以下に述べる3種類の方法があります。それぞれ、速度順になり下に行くほど高速になります。
-
Injection-Node
Injection-NodeによるA/DサンプリングはInjection-Nodeの周期(1sec単位)に規定されます。環境的なセンサなどの単発で取れるセンサ情報の取得に向いています。 -
WebSocket (Fast-Injection)
サンプリングレートがInjection-Nodeより高速に設定できるので、0.1sec単位でサンプリングしたい場合などに利用します。 -
AtomicRead
単発でのサンプリングになります。サンプリング速度をSystemの速度(USBのトランザクションタイム)と同一にしてこれ以上細かいサンプリングにできない(Atomic)ほど高速(1-2msec)にして一定期間データをサンプリングする方式です。応用例としてはある程度長期の電力量の測定などを念頭に置いて開発しました。つまり、交流測定のための50Hz/60Hzの正弦波の測定になります。サンプリング定理を考慮すると100/120Hzのサンプリング速度が必要になります。
curl "http://localhost:3000/read?target=ADC2&type=mean&sampling=128"
{"result":"success","value":545.96875,"target":"ADC2","type":"mean","sampling":"128"}
URIの中にある、type, samplinng によりオプションの設定を行います。
-
typeの種類
max, min, mean, rms, sd, rawを取ります。
それぞれ、取得したデータの最大値・最小値・平均値・実効値・標準偏差・RAWを得ることができます。 -
例(max)
サンプリングしたデータの中の最大値を取得します。
curl "http://localhost:3000/read?target=ADC2&type=max&sampling=128"
{"result":"success","value":550,"target":"ADC2","type":"max","sampling":"128"}
- 例(rms)
先に述べた電力測定の例はこちらのrmsをオプション指定することになります。
curl "http://localhost:3000/read?target=ADC2&type=rms&sampling=128"
{"result":"success","value":535.2500364899568,"target":"ADC2","type":"rms","sampling":"128"}
- 例(raw)
samplingで指定したデータを全て、そのまま出力します。
curl "http://localhost:3000/read?target=ADC2&type=raw&sampling=16"
{"result":"success","value":[533,544,527,523,543,535,530,544,526,530,547,533,532,537,533,535],"target":"ADC2","type":"raw","sampling":"16"}
I2C
Groovy-IoTはI2Cのインターフェイスを持ち、容易にI2C方式のGrove Systemと接続が可能です。現在、I2Cでサポートされているのは以下のセンサになります。プラグインモジュール(Java)を記述すれば、他のセンサでも利用可能です。
- BME280
- ADXL
- D6T
- D7S
UART
-
GPS
現状では以下の二種類のハードウェアの動作を確認しています。デフォルトの転送速度は9600bpsです。NMEAのセンテンスが流れてくるので後段で処理します。 -
秋月電子のGPS
秋月電子の方はGrove化改造を施したもので電源電圧を3.3Vに設定する必要があります。
DAC
D/Aコンバータは設定した電圧を出力する機能です。コンピュータは基本的にデジタルであり、そこからある範囲の電圧が出せることからD/A(デジタル・アナログ)コンバータになります。分解能は5bitなので、ADCと同様にVDD=5Vで設定した場合は5.0V/2^5=0.16V/bitになります。
この機能はGrove-Systemではサポートされていませんが、Groovy-IoT+kikoriでは利用することが出来ます。
system.clj から一部抜粋
...
(+device :any
(config :system {:PCB "1.0.0" :power :5.0})
(config :ADC {:vrm :VDD})
(config :DAC {:vrm :VDD})
(bind :GP0 :GPIO)
(bind :GP1 :ADC1)
(bind :GP2 :ADC2)
(bind :GP3 :DAC2)
(place :GP0 {:name "GP0"})
(place :GP1 {:name "ADC1"})
(place :GP2 {:name "ADC2"})
(place :GP3 {:name "DAC2"})
...
出力は以下のように行います。
$ curl 'http://localhost:3000/write?target=DAC2&value=20'
先のsystem.cljを使った場合はVDD=5Vだとすると、リファレンス電圧はVDDで設定されているので
5V / 2^5 * 20 = 3.125V
が出力されます。
CLKR
CLKRは本デバイス特有の機能で、比較的高速な発振器として利用可能です。Dutyを0-100%の間で25%刻みで設定でき発振周波数は24MHz, 12MHz, 6MHz, 3MHz, 1.5MHz, 750KHz, 375KHzが出力可能です。端子はGP1のみなので、それに相当するコネクタで配線します。
system.clj から一部抜粋
...
(+device :any
(config :system {:PCB "1.0.0" :power :5.0})
(config :ADC {:vrm :VDD})
(config :DAC {:vrm :VDD})
(bind :GP0 :GPIO)
(bind :GP1 :CLKR)
(bind :GP2 :ADC2)
(bind :GP3 :DAC2)
(place :GP0 {:name "GP0"})
(place :GP1 {:name "CLKR"})
(place :GP2 {:name "ADC2"})
(place :GP3 {:name "DAC2"})
...
$ curl 'http://localhost:3000/write?target=CLKR&duty=:50&frequency=:3M'
1.44inch-LCD
こちらのLCDをサポートしています。RasPiに接続したLCDに出力します。フローの開発に使うPCでの実行はWeb-browserで当該LCDを仮想的に出力します。
system.clj から一部抜粋
...
(load-module "tick")
(defn register-device []
(on-platform
;; On board
(+interface
...
;; Physical LCD
;; (config :FB {:path "/dev/fb1" :width 128 :height 128 :view :BME280})
;; Virtual Screen
(config :FB {:width 128 :height 128})
(place :FB {:name "LCD0" })
...
画像の表示は
http://localhost:3000/read?target=LCD0
で行います。
以下に仮想的に接続したD6T(オムロン人感センサ)の処理フローを提示します。仮想的と言うのは実際にセンサが接続されていないが、あたかもセンサからデータが出力されているように見せることです。kikoriから出力されるであろうデータ列を後段に与えることにより、あたかもセンサからの処理を行っているように見せます。
実際のフローは下記になります。
[{"id":"8ef43bae.efb868","type":"inject","z":"a1518d08.baa02","name":"D6T","topic":"","payload":"{ \"D6T\" : [ -100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200, 500, 200, 190, 250, 300]}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":90,"y":100,"wires":[["70cf73c6.dedc3c"]]},{"id":"a327a81.dd23758","type":"function","z":"a1518d08.baa02","name":"normalize","func":"lowTemp = 0 * 10\nhighTemp = 50 * 10;\n\n// -10degree to 100 degree\n// coming data is 10 times data (e.g. 10degree means 100)\nvar biasTemp = msg.payload + lowTemp;\nvar normalizedTemp = biasTemp / (lowTemp + highTemp);\nmsg.payload = normalizedTemp;\n\nreturn msg;","outputs":1,"noerr":0,"x":400,"y":240,"wires":[["18d42395.e52d4c"]]},{"id":"d16f385b.a0e028","type":"debug","z":"a1518d08.baa02","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":790,"y":340,"wires":[]},{"id":"70cf73c6.dedc3c","type":"split","z":"a1518d08.baa02","name":"JSON2Array","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":190,"y":180,"wires":[["6834b144.2fd71"]]},{"id":"6834b144.2fd71","type":"split","z":"a1518d08.baa02","name":"Array2Individual","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":360,"y":180,"wires":[["a327a81.dd23758"]]},{"id":"18d42395.e52d4c","type":"function","z":"a1518d08.baa02","name":"colorBarRGB","func":"gain = 10;\noffset_x= 0.2;\noffset_green = 0.6;\n\nfunction sigmoid(aX, aGain, aOffset_x) {\n return ((Math.tanh(((aX + aOffset_x) * aGain) / 2) + 1) / 2);\n}\n\naX = (msg.payload * 2) - 1;\ncolorBarR = parseInt((sigmoid(aX, gain, -1 * offset_x)) * 255);\ncolorBarB = parseInt((1 - sigmoid(aX, gain, offset_x)) * 255);\ncolorBarG = parseInt(((sigmoid(aX, gain, offset_green) + (1 - sigmoid(aX, gain, -1 * offset_green))) - 1.0) * 255);\n\nhexR = (\"0\" + colorBarR.toString(16)).slice(-2);\nhexG = (\"0\" + colorBarG.toString(16)).slice(-2);\nhexB = (\"0\" + colorBarB.toString(16)).slice(-2);\n\ncolorHex = \"#\" + hexR + hexG + hexB;\nmsg.payload = colorHex;\n\nreturn msg;","outputs":1,"noerr":0,"x":570,"y":240,"wires":[["f68814dc.527738"]]},{"id":"f68814dc.527738","type":"join","z":"a1518d08.baa02","name":"","mode":"auto","build":"string","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":"false","timeout":"","count":"","reduceRight":false,"x":430,"y":340,"wires":[["a229007b.8f065"]]},{"id":"a229007b.8f065","type":"template","z":"a1518d08.baa02","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<!-- True-Rectangle -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"128\" height=\"128\" viewBox=\"0 0 128 128\">\n\n<!-- 1st row -->\n<rect x=\"0\" y=\"0\" width=\"31\" height=\"31\" fill=\"{{payload.0}}\"></rect>\n<rect x=\"32\" y=\"0\" width=\"31\" height=\"31\" fill=\"{{payload.1}}\"></rect>\n<rect x=\"64\" y=\"0\" width=\"31\" height=\"31\" fill=\"{{payload.2}}\"></rect>\n<rect x=\"96\" y=\"0\" width=\"31\" height=\"31\" fill=\"{{payload.3}}\"></rect>\n\n<!-- 2nd row -->\n<rect x=\"0\" y=\"32\" width=\"31\" height=\"31\" fill=\"{{payload.4}}\"></rect>\n<rect x=\"32\" y=\"32\" width=\"31\" height=\"31\" fill=\"{{payload.5}}\"></rect>\n<rect x=\"64\" y=\"32\" width=\"31\" height=\"31\" fill=\"{{payload.6}}\"></rect>\n<rect x=\"96\" y=\"32\" width=\"31\" height=\"31\" fill=\"{{payload.7}}\"></rect>\n\n<!-- 3rd row -->\n<rect x=\"0\" y=\"64\" width=\"31\" height=\"31\" fill=\"{{payload.8}}\"></rect>\n<rect x=\"32\" y=\"64\" width=\"31\" height=\"31\" fill=\"{{payload.9}}\"></rect>\n<rect x=\"64\" y=\"64\" width=\"31\" height=\"31\" fill=\"{{payload.10}}\"></rect>\n<rect x=\"96\" y=\"64\" width=\"31\" height=\"31\" fill=\"{{payload.11}}\"></rect>\n\n<!-- 4th row -->\n<rect x=\"0\" y=\"96\" width=\"31\" height=\"31\" fill=\"{{payload.12}}\"></rect>\n<rect x=\"32\" y=\"96\" width=\"31\" height=\"31\" fill=\"{{payload.13}}\"></rect>\n<rect x=\"64\" y=\"96\" width=\"31\" height=\"31\" fill=\"{{payload.14}}\"></rect>\n<rect x=\"96\" y=\"96\" width=\"31\" height=\"31\" fill=\"{{payload.15}}\"></rect>\n\n</svg>\n","output":"str","x":580,"y":340,"wires":[["d16f385b.a0e028"]]}]
irMagician
USB赤外線リモコンアダプタのirMagician
の制御です。
system.clj から一部抜粋
...
(on-platform
(+interface
(config :UART {:path "/dev/ttyACM0" })
(place :UART {:name "IR0" :module :IrMagician :data-path "/tmp/irMagician"}))
...
irMagicianをUSBに接続すると、それ自身がCDC-ACMデバイスであるため、シリアルポートとして認識されます。Linux(RasPi)ではttyACMxがデバイスファイルになります。これをIR0
として抽象化します。ノーマルirMagicianのファームウェアはOSx10.4以降のドライバと相性が悪いため、Macでは使えません。irMagicianIIのファームウェアは問題ありませんが、現在はディスコンです。
system.cljの設定では、/tmp/irMagician/
を赤外線リモコンデータが格納されたディレクトリとして指定しています。このディレクトリに各赤外線リモコンデータのJSONファイルを格納します。
curl 'http://localhost:3000/write?target=IR0&op=power'
末尾のpowerが赤外線リモコンデータになります。使用に際しては拡張子の.json
を外して指定します。
赤外線リモコンデータの学習
蛇足ですが、赤外線リモコンデータの学習もkikoriから行えます。
- Captureコマンドの実行
kikoriのshellから、下記コマンドを実行します。
groovy-iot=> (irmagician.core/ir-capture)
-
学習させたいデータの送出
実際のリモコンから、irMagicianに向かって学習させたいリモコンボタンを押します。 -
リモコンデータのセーブ
kikoriのshellから、下記コマンドを実行します。
groovy-iot=> (irmagician.core/ir-save "/tmp/irMagician/captured.json")
- リモコンデータの確認
別のshellを開いて、今学習したデータが使えるかどうかを確認します。
curl "http://localhost:3000/write?target=IR0&op=captured"
カメラ
PC内蔵カメラかRasPiにUSB経由で接続したカメラ(Webカメラ)の制御が可能です。JPG形式で静止画を取得できます。
system.clj から一部抜粋
...
(load-module "tick")
(defn register-device []
(on-platform
;; On board
(+interface
;; Camera
(config :CAMERA {:index 0 :store "/tmp"})
(place :CAMERA {:name "CAM0" })
...
使い方
- kikoriの起動
system.cljの設定が出来たら、kikoriを起動します。
$ java -cp modules:kikori-1.5.0-SNAPSHOT.jar kikori.shell system.clj
- 接続されているカメラの確認
kikoriのshellから接続されているカメラを確認します。
groovy-iot> (:camera (*i))
{:index 0, :store "/tmp", :phy #object[com.github.sarxos.webcam.Webcam 0x83b55dc "Webcam FaceTime HD Camera 0x8020000005ac8514"]}
phyのところのオブジェクトが接続されているカメラになります。機種によって変わります。この場合はMacBookProに内蔵されているカメラです。
- カメラの起動
カメラの起動は別のshellあるいはweb-browserから、
shell
curl "http://localhost:3000/write?target=CAM0&value=1"
web-browser
http://localhost:3000/write?target=CAM0&value=1
カメラを停める時はvalue=0
にします。
-
画像の取得
Web-browserで下記URLを設定します。 -
JPEG
http://localhost:3000/read?target=CAM0
- PNG
http://localhost:3000/read?target=CAM0&type=PNG
- base64
http://localhost:3000/read?target=CAM0&type=BASE64
Omron USB型環境センサ(2JCIE-BU)
OmronのUSB接続型の環境センサをUSB接続のみで利用が可能です。USBでの接続はシステムからはUARTとして見えます。
system.clj から抜粋
(+interface
(config :UART {:path "/dev/ttyUSB0" :baud-rate 115200})
(place :UART {:name "2JCIE-BU0" :module :BU.2JCIE :id "34:F6:4B:66:47:E7"})
予め、センサをドライバなどをインストールして使える状態にしておきます。ドライバ関連はこちらになります。
UARTとして見えるので、シリアルポートの設定が必要になります。それぞれの実行環境に合わせたポートの設定を行います。ここではLinux環境のポート設定になりますが、Windows, MacなどでそれぞれのPC環境で設定します。
データの取得
- shell
curl "http://localhost:3000/read?target=2JCIE-BU0"
- web-browser
http://localhost:3000/read?target=2JCIE-BU0
出力結果
{"pressure":1001.932,"raw":[82,66,26,0,1,34,80,-118,-97,6,-105,20,0,0,-52,74,15,0,13,22,0,0,-112,1,-88,27,-99,7,109,11],"discomfort":68.24,"noise":56.45,"humidity":50.15,"result":"success","heat":16.93,"id":"34:F6:4B:66:47:E7","ambient":0,"eco2":144,"temperature":14.39,"etvoc":0}
URLで抽象化されているので、これをターゲット側にデプロイしても、そのまま動作します。今後はBLE対応のWebSensorServer(WSS)が来ると思うので、ターゲット毎にidを記述したWhiteListを保持し各ターゲット自身がそのセンサは自分がケアすべきかどうかを判別し、然る後にそのidで紐付いたセンサデータをハンドリングするようになるはずです。
TICK
TICKは仮想的なデバイスです。WebSocketのIntervalと連動していて、Intervalで設定した周期でOn/Offを繰り返します。先に紹介したLチカでもこの機能を利用しています。このIntervalで処理を記述することにより、あたかも同期回路のようなフローを構築することが可能になります。カウンターを設けることにより、分周器を作ることが出来るのでフローがスッキリします。
Hot-(Re)Plug
この機能はLinuxでサポートされています。RasPiなどのターゲットデバイスが対象です。Hot-Plugのユースケースはkikori起動中にGroovy-IoTを外し、センサの交換・その他作業を行い、kikori再接続を想定しています。この一連のタスクの中で、kikoriの再起動の必要はありません。
system.cljの構成
kikoriの動作はsystem.cljで定義されます。ここではユーザ設定をどのように行うかを見てみます。
-
Groovy-IoT
-
PCB Version
-
Power
は適正値に合わせます。特にPowerはADC/DACのリファレンス電圧に効いてくるので重要です。基板のバージョンもGroovy-IoTの表記に合わせておくと良いでしょう。 -
GPxのマッピング
GP0-3までのポートと機能とのマッピングになります。こちらも使いたい機能に合わせて、マッピングします。 -
UART
具体的なシリアルポートとボーレートと機能をマップします。起動時にオープンしますので、適切なポートを指定しないと警告が出ます。使用していない場合は実害はありません。OSXに関してはハブを噛ませるとtty.usbmodeXXXX のXXXX部分の桁が増えます。 -
その他
起動時に設定確認用のプロンプトのOn/Offはsystem.cljの(confirm)
部分をコメントアウトするかどうかで決定できます。
最後に
Groovy-IoT + kikoriでセンサやアクチュエータを絡めたエッジデバイスの開発がほとんどの場合でweb-api形式(URLのアクセス)で実装できます。特にPCと完全に共通化した開発・実行環境になるので、PCで開発とテスト、エッジデバイスで実運用のサイクルを構築できます。さらにenebularと組み合わせると、PCで開発したフローの変更を全くなし**(Coherent deployment)**にエッジデバイス側にデプロイ出来ます。