見出し画像

100均のリモコンからIoT始めた

どうも。しばらくノートを書いてなかったのですが、久々に書く気になったので。

今回は、あの100均に売っている、ブルートゥースシャッターで、コンセント電源の操作してみました。そんなに難しくないだろうと思ってましたが、結構ハマった箇所があったので、残しておこうかなと。
今後、誰かの参考になるように、いろいろ記しておきました。
時系列で記していきます。

きっかけ

普段通りの日常を送っていたら、たまたま物を退けたとき、小さなおもちゃをみつけました。それは、過去にガチャガチャで引いた、バスの「次、泊まります」ボタンです。実際に、ボタンを押すと音が鳴って、ランプが光るやつです。↓同じモデルのやつ

これを見て思いました:「これを押したら家電操作できたら面白そうだなぁ」と。

1.リモコン探し

いいもの発見!!

最初、これを改造してリモート操作できるようにしよう!と思ったのですが、そこまで電子工作のスキルがないことと、こんな小ささに収まらないだろうということで諦め。
Googleで調べて、いいのないかなぁと思っていましたが、こんなのを見つけました。

100円均一のBluetoothシャッター

100均で、スマホのカメラのシャッターを遠隔で操作できる商品です。スマホのカメラでは、音量ボタンを押すとシャッターが押される仕様になっているらしく、そこを利用したものだそう。
なので、カメラアプリ以外で個のリモコンを使うと、音量を操作するだけのボタンとして機能する。ただし、両方のボタンはどちらも機能としては同じで、音量UPするだけしかできない。
今回は家電操作だけで十分なので、1ボタンで問題ないのですが、せっかく2つボタンがあるのに、機能が同じなのは嫌だなぁと。
※この辺の話は本題からそれるので、「おまけ」の「リモコンの改造」に記します

2.リモコンからの信号を受信

もういちど書きますが、このリモコンは、スマホの音量ボタンを押すことにより、カメラのシャッターを切る商品です。
カメラアプリ以外では、ただの音量UP専用リモコンになります。
そこで、音量UPボタンが押されたら、遠隔操作できるようにしようという風になりました。
そこで、今回はこちらのアプリを使いました。

MacroDroidという、Android専用のマクロアプリです。
これを使って、「音量ボタンが押された」→「何かを実行」
ということが簡単に実装できます。本題からまた反れるため、使い方などは省略します。

3.電源スイッチ

電源スイッチを操作するには?

MacroDroidは、通知を鳴らしたり、電話を自動発信したり、バイブレーション、アプリを起動など、Androidの機能を呼び起こすことはできます。
当然、物理的なコンセント電源の操作はできません。
そこの部分は、Switchbotという機器を使って操作します。

↑電源の操作なので、今回はこちらを買いました。

一般的なの使用方法

一般に、Switchbotは、Switchbotというアプリをインストールして、そのアプリ上で遠隔操作を行います。機器にWifiの設定をすれば、外出先からでも、インターネットにつながる場所であれば操作ができます。
また、これにはWifi以外にBluetoothも入っていて、そこからでも操作できます。(アプリでは近距離と遠距離とで2つを使い分けてる??)

アプリ以外から操作する方法

私は、Switchbotを100均リモコンから操作したいのです。
じゃあどうすんねんと思いますが、今回はAPIというものを検討します。

APIというのは、大雑把に言うと、プログラムの機能の一部を、クラウドでやってくれるものです。
今回の場合、クラウドに当たるものは、Switchbotのサーバー(api.switch-bot.com)です。
api.switch-bot.comに、「電源をオンにしてー」と送ることで、Switchbotが実際にONになります。
仕組みを推測すると、Switchbotが数秒に1回、api.switch-bot.comに「私に命令はありますか」と問い合わせているのだと思います。(家のFirewall開けなくても動くので)
これを使っても動くのですが、「api.switch-bot.comという、外部の人間が運営するところにデータを送りたくない!!」という思いがあったのです。
別にSwitchbotが中国企業だからと言って、データが漏れる~~とかそういう話ではなくて、そこから独立させておきたいのです。
過去にAWSの障害が起きた時、api.switch-bot.comも被害を受け、一時的に使えなくなったことがありました。こういうのが嫌なのです。

アプリ以外から操作する方法2

アプリ以外かつサーバーを通らない方法で操作する方法、あります。
pythonというプログラミング言語を使って、Bluetoothで操作できるように作られたコードがあったのです。しかも、公式がね。

最終的に、これを参考にしました。

4.全体の見通しが立った

ということで、どのような仕組みにするか見通しが立ったので図示します。

ざっくりと書いた図

リモコンのボタンを押すと、Bluetoothでスマホの音量UPボタンを押す。
Macrodroidで検知し、インターネット経由で自分で建てたサーバーに送信。
最後に、サーバーがpythonを実行して、switchbotを操作する。
という流れです。
この時点で、リモコンからスマホへはできています。

5.サーバーを作ろう

node.jsで書く

ということで、サーバーを立てます。
apacheを使ってもいいのですが、今回はいい機会ですので、node.jsを使用します。(いい機会っていうのは、node.jsを使ったことがないからという個人的なやつ)

const https = require('https');
const { exec } = require('child_process');
const fs = require('fs');


const port = 443;


const cert = {
  key: fs.readFileSync('./cert/privkey1.pem'),
  cert: fs.readFileSync('./cert/cert1.pem'),
  ca: fs.readFileSync('./cert/chain1.pem')
};

const server = https.createServer(cert, (req, res) => {
  var param = new URL(req.url, `https://${req.headers.host}`).searchParams;
  
  if(check(param.get('key'))){
    
    command(param.get('q'));
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/plain');
    res.end('success');
  }else{

      res.statusCode = 404;
      res.setHeader('Content-Type', 'text/plain');
      res.end("failed");
  }
  
});

server.listen(port, () => {
  console.log(`Server running at https://example.com/`);
});


//注意 このパスワード認証はセキュリティ的によくないですので長期運用は避けるべき
function check(key){
  if(key == "password"){
    return true;
  }
  console.log("Password is incorrect!");
  return false;
}

function command(param){
  console.log(`Run!! (${param})`);
  exec('python ./py/run.py '+param, (error, stdout, stderr) => {
    if (error) {
      console.error(`run.py execution error: ${error}`);
      return;
    }
    console.log(`run.py output: ${stdout}`);
    console.error(`run.py error: ${stderr}`);
  });
}

chatgptを駆使し、大雑把ですがこんな感じ。
httpsにしていますが、これはcertbotというものを使って発行した無料証明書を使いました。※話がそれるので、本題の後に書きます。
証明書を発行しないとエラー吐いて動かないので、面倒なら、httpsをhttpに変えもいいです。

他に、セキュリティ対策のためにパスワードを使用して他者からのアクセスをブロック処理を入れました。(この辺がcheck()のところ。)ただ、こんな簡易的な認証ではセキュリティ的に問題ですので、長期運用するなら少なくともハッシュ化はすべきですね。(これ以上深くセキュリティの話をすると大きな記事が書ける分量になるのでここまで…)

また、command(param)でpythonの実行する処理に書きましたが、単純にrun.pyの引数に命令を乗せただけです。

これで、運用上、必要最低限のコードは書けたかなぁと思います。
次は、node.jsで受けたデータを基にswitchbotを操作する部分。

6.pythonで書く

さて、ここから、pythonに移ります。pythonから操作するコードがあるgitを再掲します。

よし、やるぞ!!と思い、実装しようとしました…

後に気づいたことですが、この公式のソースにはデメリットがあります。実は、LinuxベースのOSでしか使えないのです。要するに、Windowsで使えないのです。
※これに気づかず3時間くらい無駄にしました
家にはWindowsしかない&余っているPCがないので、このままでは使えません。

しかし、実際にソースコードを読んでみました…
「これ、改変次第でWindowsでも使えるな」と思いました。
Linuxしか使えないものを使用していそうだったので、そこをうまく置き換えれば動きそうです。
bluetoothのBLEには精通してないので、自分で組むには大変だなぁと。そしてネットで探してみると…

ありました!!今回は、こちらのソースコードから一部拝借いたしまして、最終的にはこうなりました。
※サーバーにBluetoothが無いなら、別途Bluetooth用のドングルを買ってください。

import asyncio
from bleak import *
import json
import sys


#pressは、toggleと同じ信号らしい。
commands = {
    'press': b'\x57\x01\x00',
    'toggle': b'\x57\x01\x00',
    'on': b'\x57\x01\x01',
    'off': b'\x57\x01\x02',
    'open': b'\x57\x0F\x45\x01\x05\xFF\x00',
    'close': b'\x57\x0F\x45\x01\x05\xFF\x64',
    'pause': b'\x57\x0F\x45\x01\x00\xFF',
}


def switchBot(address, cmd):
    UUID = "cba20002-224d-11e6-9fb8-0002a5d5c51b"
    print(cmd)
    async def run(address, loop):
        async with BleakClient(address) as client:
            y = await client.read_gatt_char(UUID)
            await client.write_gatt_char(UUID, bytearray(commands[cmd]))
    loop = asyncio.get_event_loop()
    loop.run_until_complete(run(address, loop))


if len(sys.argv) < 2:
    print("ERROR 引数が不足しています。")
    sys.exit()
    
cmd = sys.argv[1]

# JSONファイルのパス
file_path = 'device.json'

# JSONファイルを読み込む
with open(file_path, 'r') as f:
    data = json.load(f)

    
if not cmd in data:
    print(f"ERROR ${cmd} は存在しません")
    sys.exit()

device = data[cmd]

switchBot(device["macaddress"], device["command"])

commandsの連想配列は、公式のソースから拝借してます。

今後デバイスが増えることを想定して、device.jsonに、名前と、動かす機器のMACアドレス、コマンドを紐付け。実際には、こんな感じで。

{
	"plug1-toggle": {
		"macaddress": "70:04:1D:*:*:*",
		"command": "toggle"
	},
	"plug1-on": {
		"macaddress": "70:04:1D:*:*:*",
		"command": "on"
	},
	"plug1-off": {
		"macaddress": "70:04:1D:*:*:*",
		"command": "off"
	}
}

これで、プログラムを組む部分は終わり。

7.サーバーを公開!

この作業は外部(世界中どこからでも)家のサーバーにつながるようにする作業です。多少の知識がいります。(IPアドレス、ポート番号、NATとかについて知っていればok)あと、この作業は家のネットワーク環境によって作業が異なることがあります。ご容赦ください。(2023/10/19に追記しました)

どのように通信してるか、どういう作業なのか

外部からどのような経路でデータを受け取っているのか、おおまかに説明します。
外部から送られてきた通信は、まず、あなたが契約しているプロバイダーへやってきます。具体的に言うと、NURO光(sony)、フレッツ光(ntt)、BIGLOBE、携帯キャリアだと、docomo, au, softbank, 楽天もですね。
(ちなみに、「光」とついてるのは、光回線を意味する。紛らわしいが、プロバイダーとは別)
そこから、あなたのご家庭にあるルーター(正しくはCPE)に送られます。ちなみに、ネットワーク回線の工事は、プロバイダーと家を線で結ぶために必要なんですね。
そして、ルーターからあなたの端末に届きます。
携帯の4Gとか5Gとかは、プロバイダー(docomoとか)から基地局へ、そこから電波で直接端末に送ってます。

あなたがインターネットをする時、外部から情報がやってくるときは、大抵、「外部」→「プロバイダー」→「ルーターやアンテナ」→「端末」という経路でデータが来ます。ただ、少なくともプロバイダーは経由するはずです。(まあ、グローバルIPを直接買うとかすれば外部から直接できるけどね。何十万するし審査もあるけど。)

こうして外部からデータを受け取っています。しかし、外部(インターネット)は世界中いろんな人が利用しています。悪意を持った人がいることもあり得ます(ていうかいると断言してもいい)。もし、そんな悪意のある人があなたに秒速10000000GB(=10PB)もの膨大なデータを送りまくってきたらどうしましょう?残念ながら量が多すぎて機器がダウンしてしまいます。(これがDos攻撃)
あと、あなたの端末にある脆弱性(バグ)を利用して変なデータを送りつけられ、変なことされたらどうしましょう?怖いですね。
そのため、普通は知らない、関係ない通信はプロバイダーが全部シャットアウトしてくれます。無視します。
これはありがたいのですが、今回のように、「外部からの信号を受け取ってSwitchbotを動かす」ことはこれではできません。プロバイダーがシャットアウトするからです。

長くなりましたが、ここ作業では、プロバイダーがシャットアウトしている通信のうち、外部から自サーバーのnode.jsに届く通信だけ許可(通過)してあげる作業です。
また、動的に変化するIPを固定化することも含まれます。

IPv4の場合

まず、DHCP固定割り当てから行います。家庭用のルーターから支給されるローカルIPアドレス(192.168.~~とか 10.~~とか)、実は固定ではありません。これだと受信できないので、常に同じIPを支給してもらうようにします。(毎日引っ越ししてるようなもの)
PCのMACアドレスを使用して設定します。これで、ルーター(CPE)から自サーバのルートが固定しました。
次に、ポートマッピング。これは、プロバイダーから各家庭に支給される、グローバルIPが宛先の通信で、特定のポート番号で通信が来たら特定の機器にデータを送る機能です。例えば、外部から「184.100.0.0:443」というアクセスが来たら、特定の機器の、192.168.0.5:443につなぎます。ここではnode.jsが443というポート番号を使用するため、443で設定します。
これで外部からのアクセスで、家のサーバーにつながります。

しかし、まだ問題があります。
グローバルIPもたまに変わるのです。プロバイダーは各家庭に(普通は)1つのグローバルIPを割り当てています。実際にどのようなIPが割り当てられているかは、https://www.cman.jp/network/support/go_access.cgiを見るとわかります。
上の例でいう「184.100.0.0」の部分が変わります。(CGNがその一因だと思うが) セキュリティ的な理由などからこれがデフォルトだとは思いますが、家にサーバーを建てるとなると不便です。(ただし、これを固定にするよう、プロバイダーとの契約をすれば問題なし。)
そのため、いくら外部からアクセスできるとはいえ、IPが変わると家へアクセスできません。(不定期で引っ越しするようなもんです。外部から見ると失踪してしまってるように見えます。)
そこで、これを使います。

これは、無料のDDNSサービスで、○○.f5.siと、IPアドレスを結びつけることができます。
一日一回は多いですが、定期的に「○○.f5.si」と「不定期で変わるグローバルIP」を紐付ければ、○○.f5.siというURLを知っていればグローバルIPもわかります。
しかも、このサービスはありがたいことに、永久無料、回数制限なし、サーバーダウンなし、簡単に更新ができるという、最高にニーズに合ったものです。(要するに神)
これを使わない手はないでしょう。早速、登録します。
○○.f5.siの○○に好きな文字列を入れて、パスワードを入れて登録。
これだけ。
グローバルIPが変わったときでも、家のサーバーから https://f5.si/update.php?domain=ユーザ名&password=パスワード
にアクセスするだけで更新ができます。回数制限がないので、サーバーに、定期的に更新する記述でも書けばいいですね。(linuxでいう、cronとcurlを使えばすぐできそうです) 今回は面倒なので、更新は全手動でやります。アクセスするだけだし。

分かりにくかったかもですがこれで外部から家のサーバーへ繋げました。

IPv6の場合

上の説明は、IPv4の場合の説明ですが、何かに役に立つかなぁと思ったので、IPv6の場合も説明します。
しかし、ここで書くと話が長くなるので、下の方に書きます…
IPv4の話も十分長いけどね…


6.最後!!

ここまでやっておいて、一つやってないことが。
macrodroidに、「音量ボタンUPを押したら、https://~~.f5.siにアクセスする」というマクロを入れましょう。あ、パラメーターqとkeyを設定するのも忘れず。
このままだと、外出中しか使えないので、家の中でも使えるように、もう一つマクロを。「音量ボタンUPを押したら、https://192.168.0.*にアクセスする」この、192.168…のやつは、DHCP固定割り当ての時に設定したIPと同じものです。
最後に、WIFIにつながっていなかったら前者、繋がっていたら後者を実行するようにIFを追加。
(こんなに細かく書く必要はないとは思うけど一応ね。)
これで、全部できましたね!!

7.完成!!

これで、nodeを起動、やってみると、いけるかと。あ、ここの記事のソースを丸写ししてたら、多分node.jsが「証明書ないぞ!」とエラー吐くね。証明書発行は下に書いたので必要なら参考にしてください。
それ以外の原因でもし動かないのであれば、自分で修正しちゃってください…


反れた内容

本題から反れた内容について、まとめます。

リモコンの改造

100均のリモコン、2つボタンがあるのに実はどっちも同じ機能だった!!
それは嫌だなぁ。
そう思い、改造できるかな…?と。
そう思い、ググってみると…

改造して、音量下げボタンにするという記事を見つけました。
お、これはいけるぞ!!と思い、100均に行って、2個買ってきました。1つは改造用、もう一つは予備用です。(実際に買ったのが写真のやつ)
さあ、改造していこう…と思いましたが、大きな落とし穴がありました

改造のできるリモコンとできないリモコン

よーし、買ったぞ~と思い、さっそく改造すべく、中身を空けました…
しかし…

ICが…違う!?

写真を撮ってないのでここには載せませんが、記事の写真とは別の回路だったのです。スイッチの位置、ボタン、ICの位置などは一緒でしたが、記事に載っているICとはまた別のものが使われていました。
※ICというのは、この記事で言う黒い四角い部品です。

めげずに改造方法を調べましたが、どうやら私と同じ状況になった人が既にいて、その記事がありました。

私が買ったものと全く同じものです。
そして、この記事曰く、改造して音量DOWNにすることはできないとのこと。

くそ!!と思いましたが、仕方ないので置いておきます。
リベンジをすべく、今度はちゃんとダイソーに行って、探しました。すると、220円の別のリモコンを見つけました。機能は全く同じですが、これも改造記事とはまた別の、3種類目のものです。


↑これは改造記事ではなく、レビュー記事ですが、これと同じものを買いました。もちろん、改造用と予備と2個買いました。
ということで、改造開始!と思い、開けてみました…
しかし

ICが…違う!?

しかも、改造記事のリモコンのICのピン数(16ピン)より少ない、8ピンでした。もう怪しいですね…
これも調べてわかったことですが、これも改造できないタイプのモデルだそうです。
ということで、ここまでで合計4つのリモコンを買いましたが、3つが無駄になりました。

まあでも、4つで1100円ですので、大きな損失ではなかったですがね。

certbotで無料証明書

SSL(TLS)で、暗号化通信をするには、証明書があるとより安全です。なぜあったほうがいいのかは公開鍵暗号の話になり、面倒なので省略します。
そして、今まで私は「証明書を発行するのは有料」だと思い込んでいました。

こんな感じでね。
無料SSLという言葉は聞いたことがあるのですが、これはレンタルサーバーを契約したときにセットでついてくるものとおう認識で、結局はお金か~と思いました。
しかし、暗号化のためにお金を払うのはなんか嫌でしたので、オレオレ証明書作って、スマホにインストールしようかなぁと考えたわけです。しかし、どうやってもうまいこと行かず、ここで1日潰れました。
色々調べてると、「LetsEncrypt」というワードを発見。検索すると、発行が無料の証明書があったのです。

キターーー

ということで、さっそく。
コマンドプロンプトで、まずcertbotをインストールします。

winget install certbot

コマンドプロンプトで↑を打ちます。
これでインストール完了。
ここから、公式の説明通りにやっていきます
※ここでは、無料DDNSでドメイン取得済みなのが前提です。

次に、これを打ちます。

certbot certonly --standalone

この際、ポートマッピングで80番ポートをこのPCに結んでおいてください。
初回のみ、利用規約の話とかが質問されますので、YESを選ぶ。
ドメインを入力するよう指示があったら、取得した**.f5.siを入力。
certbotのサーバーから、家のPCに直でアクセスし、ドメイン認証をします。すると、あっという間に証明書が発行されました。

発行された証明書は、C:\Certbot\live\**.f5.si\にあるよ~と出てきますが、実際にあるのはシンボリックリンクで、実際のファイルはC:\Certbot\archive\**.f5.si\にあります。(私の場合はね)

最後に、node.jsから証明書のパスを指定してやればOK.
絶対パスでも相対パスでもいいけど、私は

mklink cert C:\Certbot\archive\**.f5.si\ /j

をして、シンボリックリンクで結びました。

IPv6で通信

外部から家のサーバーに、IPv6を使う場合の説明。
まず、IPv6とは?超ざっくりいうと、IPv4は「192.168.0.1」のように、0~256の数字を4つ並べて表すインターネット上の住所。
IPv6は、「2001:d70:c4:a301:892f:92c:9812:3ee8」のように、16進数の0000~ffffの65536通りの数字を8個並べて表すインターネット上の住所です。
前者は4294967296通りの住所、後者は340282366920938463463374607431768211456通りの住所を表せます。
どっちも十分多いですが、IPv4の場合、約43億通りです。全世界の人口は現在約80億人で、インターネットを使っていない人を除いても、少なくとも数十億人はインターネットを使っていると見積もられます。
察しのいい人は気づきますが、43億じゃ少ないんです。
43億通りとはいえ、全部が使われるわけではありません。(0.0.0.0や、127から始まるもの全部など。) 特定の数字(正しくはビット列)から始まるIPアドレスに意味を持たせて使用のルールを策定するなど、円滑な通信をするために必要なことをしていくうちに、IPv4アドレスの割り振りの限界がきてしまいました。
ちなみに、プライベートIPとかグローバルIPとかそういうのは、IPアドレス不足から生まれたという話もあります。(どこで聞いたか忘れた)
そこで、IPv6が生まれました。340282366920938463463374607431768211456通りもあると、使い放題ですよね(言い過ぎ)。
IPv4と同様、IPv6も特定の数字(ビット列)から始まるアドレスに意味を持たせ、まるごと使用のルールを持たせるなどをしています。例えば、fe80から始まるものは、IPv4のプライベートIPのようなものに相当します。
あと、2or3から始まるもの(正しくは001から始まるビット列)ものは、インターネット上にある一意の機器を示すためのアドレスです。

これ以上IPv6の話をすると記事の量が莫大になるのでここまでにしておきます。ここから、IPv6での実装を説明します。

まず、前提として1つ。
IPv4とIPv6は全く関連のないものです。互換性がありません。
IPv4で通信するなら、プロバイダーも相手も全てIPv4で通信しなければいけません、IPv6も然り。
(あと、IPv4よりIPv6が優れている、速度が速い!!というのは違います。あまり関係ありません。)
というわけで、上の方で示した設定は、IPv4用であって、IPv6用はまた異なった手順が必要です。また、プロバイダーによって設定方法も異なります。

まず、自分のサーバーが「2or3から始まるIP」を持っている場合。(大半の人は2001だと思います)これは、世界中でたった一つのIP(グローバルユニキャストアドレス)ですので、これさえわかれば外部から自分のサーバーにデータを送れます。
しかし、これは常に固定とは限りません。詳しいことは知りませんが、多分固定ではありません。セキュリティ関連もありますし。
しかし、IPv4の時も出しましたが再掲

これで解決。
一日一回は多いですが、定期的に○○.f5.siとそのIPを紐付けすれば大丈夫。
多分、これだけでいける。

次に、「fe80から始まるIP」を持っている場合なのですが、これは、限られた範囲(家庭内や組織内)のみ有効なアドレス(リンクローカル IPv6アドレス)です。限られた範囲のみ有効ですので、これではできません。
また、普通、先ほどの「2or3から始まるIP」を割り当てられていますので、わざわざこれを使う必要はないと思います。

最後に、IPv6の設定について。プロバイダーによっては、IPv4とIPv6で設定項目が異なります。IPv4と同じ手順で設定しても、うまくいきません。IPv6用の設定項目から、設定してあげましょう。
宛先が、自分の「2or3から始まるIP」(グローバルユニキャストアドレス)の時、かつ、宛先ポート番号が、node.jsが使用する443の時に許可(通過)するようにします。
これで、いけるでしょう。

おわりに

こんなに長い記事になりましたが、ようやく書きたいことは掛けました。
実は、これを実装するのに、1日平均5~6時間、これを2週間弱も時間をあてたんですからね!!根気のいる作業でしたよ。ほんと。
大きく省略してますが、「あれ動かない…なんで?」っていうのが山ほどありまして。全体の半分以上はこれを探るために充てたんですよ!!
今後同じことをやってみたいなぁと思う方、しんどい思いをするかもしれないぞ…
あと、実装までの流れだけでなく、その周辺情報をたんまり入れてしまったので、知見が共有出来たらいいな~と。
あ、今更ですが、ソースコードをまとめたリポジトリ用意しておきました。
※前述したように、これをそのまま実行することはセキュリティ上安全ではありません。あくまでも何かの参考程度にしてください…

というわけで、おわり。さよなら~


この記事が気に入ったらサポートをしてみませんか?