はじめに
こんにちはー!なたでです!
セカンドライフ技術系 Advent Calendar 2018の22日目の記事です。
今回も引き続きIoT関係の記事となります。
皆さん、IoTといえば電源制御をしてみたいですよね。というわけで今回は、IoTで電源制御の方法を解説し、最終的にはセカンドライフでリアルに温もりを感じてみることをやってみたいと思います。
本記事は、「セカンドライフのドアベルを押したらリアルで音が出すのを作る」からの続きとなります。
電源制御とは
電源制御について
電源制御というのは、電気をONとOFFで動く装置をIoT機器に取り付けて、外部からONとOFFをするものです。
部品/基盤の調達について
リレーという装置を使うことで実装が行えます。リレーには主に以下の種類があります。
リレーの種類
メカニカルリレー
メカニカルリレーは、非常にシンプルで分かりやすい作りです。具体的には、片方でコイルに電気を流すと電磁石になり、磁力で空間的に離れたスイッチを物理的に作動させるというものです。動作させると、スイッチが接触する際にカチっという結構大きな音がします。車の方向指示器(ウィンカー)のカチカチもメカニカルリレーです。今の車はあえて別の方法で音を発生させているようですが。
ソリッドステートリレー(SSR)
ソリッドステートリレー(SSR)は、光を発生させる発光ダイオードと、光を受けると電気を流すフォトダイオードを組み合わせたリレーです。メカニカルリレーは、スイッチが物理的に絶縁されていますが、このタイプはダイオードを利用しており厳密には絶縁されておりません。メカニカルリレーと違って、電源のONとOFFは静音です。素子が熱をもつ特徴があるため、設置する際は風通しに注意してください。
リレーについて紹介しましたが、上記のような部品単体だけあっても突入電流やら、回路を組むのは非常に難しいです。そのため、秋月電子通商やAmazonなどから部品を組み合わせたからリレーの基板キットやRaspberry Pi用電源リレーボード買うのが現実的となります。基板キットを購入する際は以下の点に注意する必要があります。
基板キットを購入する際の確認点
動作電圧の確認
「電源電圧:5V(≒リレーコイル電圧)」と書いてある場合、ラズパイは3.3Vなので直接接続しても使えません。Arduinoは5Vなので使えます。もし、3.3Vで使いたい場合はトランジスタを使った回路を作る必要があります。
駆動電流の確認
リレーを動作させるために、必要な電流も確認が必要です。ラズパイでは1ポートで出力する際の最大電流が16mAとなります。
負荷の確認
データシートに 250V AC/5A まで接続できる記載されている場合、250V×5A=1000Wで、日本はAC電源が100Vなので、10Aまで接続可能なように見えますが、5Aと書いてあるので5Aまでにとどめたほうがいいかと思います。(専門家でないため、確証は得られません。)
基板キットやリレーに関して自分で作成する場合は、ラズパイとリレーなどでググってみて、使えそうな部品を調べてみるとよいです。
なお基板キットや専用ボートなどは、配線がむき出しの状態になります。AC電源100Vで感電したりすると、命の危険に関わるため十分気を付けてください。基板ではなく、ケースや配線含めて製品となっている商品もありますが、電安法によって販売事業者としては制限があり、販売されている製品自体が少ないです。海外製品などを個人輸入で買うといった手もあります。
参考
電気用品安全法について
電安法と呼ばれる法律です。消費者を守るための法律で、販売事業者や製造事業者に対して、危険なものを作らない、売らないように決められたものです。PSEマークがついているものは、安全な商品であるといった判断基準になります。消費者は色々なことをしますから、使用方法によっては危険なことをするかもしれません。そのようなことをできる限り防止しようという注意喚起を説明書に書かないといけないとか、そういう決まりがあります。
電源制御を組み合わせたシステムを作り、もしそれを売ろうと考えた場合、電安法として危険であるため販売に問題がある場合があります。今回は事業者ではなく個人で作るものとなるため、電安法は関係がありませんが、それでも危険な物を作ることができてしまう可能性があることを意識したうえで、作る必要があると思います。
参考
PSEの目的や詳細を知りたい方は、以下の記事を参照するとよいでしょう。ちなみにIoTがらみで最近この辺の法律がどんどん変わっており、今後許可が増えてより扱いやすく便利な製品が出てくる可能性があります。
- 2006/1/30 サウナでゴリラと二人きり – [電気用品安全法]「電気用品安全法」について経済産業省に電話で問い合わせてくれた人がいるよAdd Star
- 2013/5/13 家電Watch – 経済産業省、スマホの遠隔操作による家電製品の「電源ON」を一部許可
- 2016/7/6 Qiita – 3GPIで遠隔操作OAタップ~関連法規も調べ、問合せもしてみた
- 2018/1/31 日々是おやっとさぁ – スマートコンセントのPSE対応
- 経済産業省 リスクアセスメント・ハンドブック
電源制御用ハードの準備
今回、ラズベリーパイとリレーで電源制御してみたいと思います。以下代表的なものを紹介します。他にもあったほうがいいものは「セカンドライフのドアベルを押したらリアルで音が出るのを作る」の記事を参考にしてください。
用意するもの
Raspberry Pi
家にあったRaspberry Pi Model B+を使用します。ピンから線だししやすいようにT型拡張ボードもつけました。
リレー
今回は、リレー用キットを使用するのではなく製品となっている「PowerSwitchTail II」を使用しました。
接続用ワイヤーなど
ブレッドボードとリレーとを繋げるための配線です。今回で言えば、ブレッドボードから配線を伸ばすため、ジャンパーワイヤ(オス-オス)のみあればよいです。メスのワイヤーであれば、ラズパイにT型拡張ボードでブレッドボードを経由しなくても、直接ピンに指すことが可能です。もしものときにいろいろ揃えておくと楽です。中国のやつは配送が1か月ぐらいかかりますが、謎の技術により超安いのでオススメです。
65pcs ワイヤー ブレッドボード プラグ ワイヤ ブレッドボード·ケーブル ブレッドボード タイライン
ブレッドボード,ジャンパーワイヤ(メス-オス)(20cm)40本
配線接続
ラズベリーパイ側
イメージ的にはラズベリーパイ側のポートそのものが電気信号をON/OFFさせるような制御にしたいのですが、実際にはそういう機能はもっていません。ただ、ラズベリーパイには指定したポートの電圧をHigh(3.3V)かLow(0V)に設定するという機能があり、これを利用します。この機能は、GPIO(General-purpose input/output)、汎用入出力と呼びます。
0Vは、GND(グランド)との電圧差が0Vという意味です。最初は混乱しますが、ちょっと用語を解説します。他にも、いろいろありますが、まずはこんな感じで。ちなみに〇Vと書いてある部分と、GNDを途中何も部品付けずにワイヤーで接続すると、ショートして機械が故障する可能性があり、最悪火事になりますので注意を。
開放/オープン
物理的に配線が繋がっていない状態
短絡/ショート
物理的に配線が繋がっている状態でかつ、抵抗を付けていない状態。電気が集中して流れてしまい過電流で故障します。
GND
基準となる電圧
〇V
GNDとの電圧の差。例えば+3.3Vであれば、GNDと電圧との差が+3.3Vを示す。
ラズベリーパイでは簡単にGPIOを操作できるピン番号(CPUのポートにささっている)が決まっており、今回はこれを使用して操作しましょう。なおピン配置は、以下のようになっています。GPIOとついているポート番号を利用しましょう。
T型拡張ボードがついていると、どれがどのピンなのか分かりやすいのでおすすめです。
GPIOについて補足解説
今回説明したポートの話なのですが、実はHigh/Low(以下、H/L)を制御する以外にも、組み込み向けCPUには、いくつか情報や機能を持っている場合があります。入力ポート/出力ポートについては、おそらくどんなCPUでも設定できるかと思いますが、それ以外の設定はラズパイのCPUでは関係ない話かもしれません。ただ、全て同様の機能で動くGPIOであると考えないように、ポートの特性を調査したうえで利用することが大事です。
入力ポート/出力ポート
ポートは、入力と出力を変更することが可能です。今回使用するのは、HとLを切り替えする出力機能とよばれるものです。入力機能に設定した場合は、その入力用ポートへの電圧によって、ラズパイ上からHかLかを取得することができます。なお、出力ポートにすることで電圧をかけることができますが、流せる電流はそれほど多くはありません。ラズベリーパイの場合は1ピンあたり、16mAとなります。また、同時に流れる電流は合計50mAを超えてはなりません。
補足ですが、Arduinoの入出力ポートは、1ピンあたり5Vとなり20mA。同時に流れる電流は合計100mAと大容量となっています。Arduinoは、ラズパイにはないアナログ入出力ポートも兼ね備えているため、ラズパイと組み合わせると非常に強力です。
オープンドレイン出力
出力ポートの種類にオープンドレイン出力用ポートというのがあります。このポートは電流を大幅に流せることで直接LEDを点灯することが可能なようです。ソフト的にLに設定した状態が、ハードウェア的には電圧がかかった状態になるようです。
入力プルアップ抵抗内蔵ポート
入力ポートにプルアップ抵抗が内蔵したものです。入力の電圧がHだと、ソフト的に取得した値はLに。入力電圧がLだと、ソフト的に取得した値がHになるようなポートです。
実は正直なところオープンドレイン出力というのは、私自身あんまりよく分かっていません。まだまだ勉強しないといけませんね……。
参考
- SUDOTECK – オープンドレインのはなし
- 猿まね電子工作 – ちょっとわかりにくいプルアップ抵抗について
- ツール・ラボ – 第22回 Raspberry PiのGPIO概要
- スイッチサイエンス – Arduino Uno R3
リレー側
データシートを確認しましょう。データシートとは、その回路図の使用方法などが記載されたものです。もしデータシートを見ても接続方法が分からない場合は、製品名や基板名でググりましょう。通常、誰かが接続させた例が出てきます。
さて、今回は「PowerSwitchTail II」で接続をしたいと考えています。これをググると超分かりやすいサイトが出てきます。どうやら「PowerSwitchTail II」の2つの端子に、ラズパイを直接繋げればよいそうです。「1:+in]を出力用GPIO。「2:-in」をGNDです。ねんのため、データシートをみて正しいかどうかも確認しました。以下のような情報を読み取りましたので、ラズパイで直接接続しても大丈夫です。
- 入力電圧 3-12V、使用電流 3-30mAで動作。
- +と-の間に抵抗が挟まっていることから直接接続しても大丈夫。
- 通電確認用の発光ダイオードによって、ONとOFFが分かるようになっている。
- 「A1」ブロックが謎ですが、左側のメカニカルリレーで制御している感じ。
- リレーの遅延は接続時最大15ms、オープン時最大10ms。寿命は100,000回。
- 120V/15AまでのAC電源の配線を制御可能。
- 16 AWG以下のワイヤーを使ってください。
- 配線が露出していない安全設計。
参考
接続する
上記を踏まえて、GPIO17にワイヤーを使って接続してみます。+inはGPIO17に、-inはGNDに繋げました。
なお一般的に、黒はマイナス。白はプラスに接続します。以下、よくあるパターンです。
- 白と黒の配線なら、白はプラス、黒はマイナス。
- 赤と白の配線なら、赤はプラス、白はマイナス。
- 赤と黒の配線なら、赤はプラス、黒はマイナス。
参考
電源制御用プログラムの準備
ラズパイ上でGPIOを操作ということで、Raspbian上で動作させる方法について説明します。
LinuxではWindowsで言うファイル、デバイス、CPUの状態なにやらなにまで、ファイルを使用して入出力/設定などが行えます。今回のGPIOの操作では、GPIO用のファイルを変更操作して、制御を行います。
GPIO操作用シェルスクリプト
今回は簡単に操作ができるように以下のようなシェルスクリプトを挟みたいと思います。
powerSwitch.sh
#!/bin/sh # 二重起動防止 pid=$$ filepath=$0 if test $pid != `pgrep -fo "${filepath}"` ; then return 1 fi # GPIOを使用できるようにする setupGPIO() { local gpio_port=${1} local gpio_direction=${2} local gpio_directory=/sys/class/gpio/gpio${gpio_port} # 引数チェック if test "${gpio_direction}" != "in" -a "${gpio_direction}" != "out"; then echo "error" return 1 fi # ファイルがなければ作成 if test ! -d "${gpio_directory}"; then echo "${gpio_port}" > "/sys/class/gpio/export" sleep 0.2 echo "${gpio_direction}" > "${gpio_directory}/direction" sleep 0.2 fi return 0 } # GPIOを除去する removeGPIO() { local gpio_port=${1} local gpio_directory=/sys/class/gpio/gpio${gpio_port} # ファイルがあれば削除作成 if test -d "${gpio_directory}"; then echo "${gpio_port}" > "/sys/class/gpio/unexport" sleep 0.2 fi return 0 } # GPIOの出力を変更する outputGPIO() { local gpio_port=${1} local gpio_output=${2} local gpio_directory=/sys/class/gpio/gpio${gpio_port} # 引数チェック if test "${gpio_output}" != "high" -a "${gpio_output}" != "low"; then echo "error" return 1 fi # ファイルがなければエラー if test ! -d "${gpio_directory}"; then echo "error" return 1 fi if test "${gpio_output}" = "high" ; then echo 1 > "${gpio_directory}/value" else echo 0 > "${gpio_directory}/value" fi return 0 } # 引数を代入 gpio_port=$1 gpio_output=$2 # 引数チェック if test "${gpio_output}" != "high" -a "${gpio_output}" != "low" -a "${gpio_output}" != "remove"; then echo "error" return 1 fi # 除去する if test "${gpio_output}" = "remove"; then removeGPIO ${gpio_port} return 0 fi # GPIOを出力ポートにする setupGPIO ${gpio_port} out if test $? -ne 0; then return 0 fi # 出力ポートの値を設定する outputGPIO ${gpio_port} ${gpio_output} if test $? -ne 0; then return 0 fi return 0
powerSwitchOn.sh
#!/bin/sh # シェルスクリプトがある場所をカレントディレクトリにする cd `dirname $0` # GPIO17 を High ./powerSwitch.sh 17 high
powerSwitchOff.sh
#!/bin/sh # シェルスクリプトがある場所をカレントディレクトリにする cd `dirname $0` # GPIO17 を Low ./powerSwitch.sh 17 low
powerSwitchRemove.sh
#!/bin/sh # シェルスクリプトがある場所をカレントディレクトリにする cd `dirname $0` # GPIO17 を削除 ./powerSwitch.sh 17 remove
これで例えば以下のように実行するとGPIO17がHIGHになります。
./powerSwitchOn.sh
参考
サーバーB (192.168.1.11, 無線、部屋起き用、Raspbian)
サーバー用のソフトは、以下のように書き換えます。構成及び変更前のプログラムは前回の記事を参照してください。今回は、内容が複雑化してきたため、既存のコードをクラスで書き直してみました。
RoomServer.js
const exec = require("child_process").exec; // jsファイルが置いてある場所 const curdir = __dirname; // 音を鳴らすクラス class PlaySound { constructor() { this.start_time_ms = 0; this.sound_time_ms = 5000; } play() { // 5秒間は次の鳴動をしない。 const now_time_ms = new Date().getTime(); const delta_time_ms = now_time_ms - this.start_time_ms; if(delta_time_ms >= this.sound_time_ms) { exec(curdir + "/playSound.sh"); this.start_time_ms = new Date().getTime(); } } } // ON/OFF用のキュー。無駄にたまっている場合は解除する。 class BooleanQueue { constructor() { this.queue = []; } enqueue(x) { if(this.queue.length === 0) { this.queue.push(x); return; } // 最後に積んだものがxの場合はつまない if(x[this.queue.length - 1] === x) { return; } this.queue.push(x); // 最初が true false true なら true にまとめる // 最初が false true false なら false にまとめる if(this.queue.length >= 3) { if(this.queue[this.queue.length - 1] === this.queue[this.queue.length - 3]) { // 上から2個取り出す this.queue.pop(); this.queue.pop(); } } } dequeue() { if(this.queue.length === 0) { return null; } return this.queue.shift(); } } // 電源制御系クラス class PowerControl { constructor() { const that = this; this.queue = new BooleanQueue(); const anzen_off_time_ms = 60 * 60 * 1000; let last_on_time_ms = 0; let is_on = false; const interval = function() { { const now_time_ms = new Date().getTime(); const delta_time_ms = now_time_ms - last_on_time_ms; // ONのまま安全時間を過ぎると、OFFになる if(is_on && (delta_time_ms >= anzen_off_time_ms)) { that.off(); } } { const data = that.queue.dequeue(); if(data === null) { return; } is_on = data; if(is_on) { exec(curdir + "/powerSwitchOn.sh"); last_on_time_ms = new Date().getTime(); } else { exec(curdir + "/powerSwitchOff.sh"); } } }; this.timer = setInterval(interval, 5000); } on() { this.queue.enqueue(true); } off() { this.queue.enqueue(false); } } // サーバー用クラス class WebServer { constructor(port) { this.port = port; this.http = require("http"); this.url = require("url"); this.server = this.http.createServer(); this.function_map = []; const that = this; const onRequest = function(req, res) { const url_parse = that.url.parse(req.url, true); res.writeHead(200, {"Content-Type" : "text/plain"}); const filename = url_parse.path.split(/[\\/?#]/); if(filename.length > 1) { res.write(""+ that.analysis(filename[1], url_parse.query)); } res.end(); }; this.server.on("request", onRequest); this.server.listen(this.port); } addFunction(function_data) { this.function_map.push(function_data); } analysis(name, query) { if(name === "favicon.ico") { return 0; } for(let i = 0; i < this.function_map.length; i++) { const function_data = this.function_map[i]; if(name !== function_data.name) { continue; } if(function_data.type && function_data.type !== query["type"]) { continue; } function_data.start(); } return 0; } } // 起動時はGPIO用のファイルがない状態にする。 exec(curdir + "/powerSwitchRemove.sh"); // Webサーバー作成 const server = new WebServer(62620); const playsound = new PlaySound(); server.addFunction( { name : "playSound", start : function() { playsound.play(); } } ); const power = new PowerControl(); server.addFunction( { name : "power", type : "on", start : function() { power.on(); } } ); server.addFunction( { name : "power", type : "off", start : function() { power.off(); } } );
これをNode.jsで立ち上げると、ブラウザ上からURLで操作することができるようになります。
音を鳴らす電源をONにするhttp://192.168.1.11:62620/playSound電源をOFFにするhttp://192.168.1.11:62620/power?type=onhttp://192.168.1.11:62620/power?type=off
サーバーA(192.168.1.10, 公開用常時起動、有線接続、CentOS7)
サーバー用のソフトは、以下のように書き換えます。
MainServer.js
const exec = require("child_process").exec; const http = require("http"); const url = require("url"); const server = http.createServer(); const analysis = function(name, query) { if(name === "favicon.ico") { return 0; } if(name === "SecondLife") { if(query["type"] === "playSound") { exec("curl http://192.168.1.11:62620/playSound"); } else if(query["type"] === "powerOn") { exec("curl http://192.168.1.11:62620/power?type=on"); } else if(query["type"] === "powerOff") { exec("curl http://192.168.1.11:62620/power?type=off"); } } return 0; }; const onRequest = function(req, res) { const url_parse = url.parse(req.url, true); res.writeHead(200, {"Content-Type" : "text/plain"}); const filename = url_parse.path.split(/[\\/?#]/); if(filename.length > 1) { res.write(""+ analysis(filename[1], url_parse.query)); } res.end(); }; server.on("request", onRequest); server.listen(62620);
実行中に、ブラウザで以下の操作ができたら成功です。
音を鳴らす電源をONにするhttp://192.168.1.10:62620/SecondLife?type=playSound電源をOFFにするhttp://192.168.1.10:62620/SecondLife?type=powerOnhttp://192.168.1.10:62620/SecondLife?type=powerOff
セカンドライフから操作する
今回は、実験で、何かをしたときに「電気あんか」のスイッチが入り、リアルでも温めるようなIoT機器を作ってみます。
「電気あんか」は電源がONになりっぱなしでも事故がおきたりしないよう設計されています。ただ、もしものため実際にテストする際は、必ず人がいるときにあんかのコンセントをさし、人がいない場合は外すようにして下さい。「電気あんか」は故障していないものを使用してください。
作成事例
セカンドライドで動作する温かみをONにする例をプログラムと感想を一緒に紹介します。
実験1 自分が物の上に座ったら電源をつける
このスクリプトをいれたものをHUDとして身に着けてください。どこかに座ったときに電源をONにします。
スクリプト
integer is_sit_on = FALSE; setPower(integer is_power) { string url="http://secondlife.hogehoge.com:62620/SecondLife?type="; if(is_power) { url += "powerOn"; } else { url += "powerOff"; } llHTTPRequest(url, [], ""); } integer is_on_1 = FALSE; integer is_on_2 = FALSE; toOneShot(integer is_on) { is_on_2 = is_on_1; is_on_1 = is_on; if(is_on_2 != is_on_1) { setPower(is_on_1); } } checkMyAvater() { string my_animatin = llGetAnimation(llGetOwner()); toOneShot(my_animatin == "Sitting"); } default { state_entry() { llSetTimerEvent(5.0); } timer() { checkMyAvater(); } }
感想
ベッドや、こたつなどどこかに座ったときにONになることを想定して作りました。何に座っても動作するというメリットがありますが、何に座っているかは判別ができないためONにしてほしくないときもONになることや、自分がHUDとして装備しているため能動的ではないというデメリットがありました。
実験2 物体自身が私が座ったとき電源をつける
このスクリプトは編集可能でかつ座れるオブジェクトに追加してください。
スクリプト
setPower(integer is_power) { string url="http://secondlife.hogehoge.com:62620/SecondLife?type="; if(is_power) { url += "powerOn"; } else { url += "powerOff"; } llHTTPRequest(url, [], ""); } integer is_on_1 = FALSE; integer is_on_2 = FALSE; toOneShot(integer is_on) { is_on_2 = is_on_1; is_on_1 = is_on; if(is_on_2 != is_on_1) { setPower(is_on_1); } } checkMyAvater() { integer link_count_max = llGetNumberOfPrims() + 1; integer link_number = 0; integer is_exist = FALSE; for(link_number = 0;link_number < link_count_max;link_number++) { if(llGetLinkKey( link_number ) == llGetOwner()) { is_exist = TRUE; } } toOneShot(is_exist); } default { state_entry() { } changed(integer change) { if(change & CHANGED_LINK) { checkMyAvater(); } } }
感想
これをベッドに入れて確認しました。なんというかベッド主導で電源をONにしてくれるというところが気持ち的によいですね。セカンドライフの中でベッドで寝ている私と、リアルでベッドに入っている私がリンクして、ちょっといつもと違った体験ができました。
実験1とは、動作している物自体は同じなのに感じる感覚が違うというのが不思議ですね。自分の脇を触っても笑わないのに、誰かに触ってもらうとくすぐったいという現象と似ている気がします。
実験3 物体自身が私と誰かが座ったとき電源をつける
このスクリプトは編集可能でかつ座れるオブジェクトに追加してください。
setPower(integer is_power) { string url="http://secondlife.hogehoge.com:62620/SecondLife?type="; if(is_power) { url += "powerOn"; } else { url += "powerOff"; } llHTTPRequest(url, [], ""); } integer is_on_1 = FALSE; integer is_on_2 = FALSE; toOneShot(integer is_on) { is_on_2 = is_on_1; is_on_1 = is_on; if(is_on_2 != is_on_1) { setPower(is_on_1); } } integer llIsAvatar(key uuid) { list details = llGetObjectDetails(uuid, [OBJECT_CREATOR]); return llList2Key(details, 0) == NULL_KEY; } checkMyAvater() { integer link_count_max = llGetNumberOfPrims() + 1; integer link_number = 0; integer is_exist_1 = FALSE; integer is_exist_2 = FALSE; for(link_number = 0;link_number < link_count_max;link_number++) { key avatar = llGetLinkKey( link_number ); if(avatar == llGetOwner()) { is_exist_1 = TRUE; } else if(llIsAvatar(avatar)) { is_exist_2 = TRUE; } } toOneShot(is_exist_1 && is_exist_2); } default { state_entry() { } changed(integer change) { if(change & CHANGED_LINK) { checkMyAvater(); } } }
感想
あら^~
なんというか、もうこれは現実と繋がった、いやセカンドライフが現実です。最高でした。
おわりに
最後まで読んでいただきありがとうございます!セカンドライフが好きな方はIoTに、IoTが好きな方はセカンドライフに興味をもちましたでしょうか!
今回2回にわたって、セカンドライフとIoTを結びつける記事を書きました。今度は、リアルのデータをSL上に反映とかそういったこともチャレンジしていきたいですね。
では、また!
コメント