【自作スマート家電】1階から2階の家族を呼び出せるシステムを作った話

  • URLをコピーしました!

皆さんこんにちは。yossyです。

最近、「2階にいる家族にLINEをしても返事がない…」という悩みを家族が話していました。これは電子工作で解決できるんじゃない?と思ったので実際にやってみました。

ということで、Raspberry Pi PicoWを使った呼び出し装置を作る方法をまとめてみます。必要な材料やプログラムを解説するので、困ってる人はぜひ作ってみてください。

目次

1階と2階は実質的に別の家

僕が住んでいる家は2階建てです。
1階と2階、同じ家の中にあるけれど、生活圏としては完全に別エリア。

1階と2階の壁

  • 階段を使った移動が必要
  • 音と光が届かないので、様子がわからない
  • 直接呼び出すならLINE

これを見ると、1階と2階の間にはかなり大きな壁があることがわかります。

基本的にはLINEで連絡すればいいのですが、僕の家族のようにあまりスマホを使わない人もいます。
そんな人は、いつの間にかスマホの電源が切れていたり、通知がオフになっていたりとLINEすら繋がらないことがあります。

なので、相手の状況にかかわらずに、こちらから一方的に声をかけられるシステムがほしい。

インターホン・見守りカメラは意外と疲れる

「ならインターホンでも導入するか!」
ちょっと待ってください。インターホンにはいくつか問題があります。

インターホンは結局移動しないといけない

インターホンは設置したボタンを押さないといけません。つまり、呼び出すためには立ち上がる必要があります。
料理中にボタンを押しに移動したり、リビングでゆっくりしてるときに立ち上がってボタンを押したりするのは面倒くさい!

スマート家電の独自アプリという罠

WiFi対応のインターホンや呼び出しベルも販売されています。とはいえ安い製品だと中華製の使いにくいアプリをインストールする必要があります。もちろんすべてのアプリが使いにくいわけではありませんが、アカウントを作成して、製品を登録して…という手順を踏むのは割と大変です。

見守りカメラのプライバシーの問題

ではインターホンではなく見守りカメラならどうでしょうか?というのも見守りカメラなら、自由なタイミングで通話して声をかけることができます。

ただ、見守りカメラを言い換えれば「監視カメラ」です。さすがにずっと自分の様子を見られるのは気になりますよね。

自作のメリット:シンプルで自由

ということで呼び出しシステムは自作したほうが使いやすいとわかります。

自作呼び出しベルなら、スマホから呼び出せるようにできるし、UIも比較的自由に作ることができます。
ですが、僕はもう一つ大きなメリットを感じました。それがこちら。↓

呼び出し音声の自由さ

呼び出すとき、自分で使う音声を決められるととても便利です。
「お風呂に入って」「1階まで降りてきて」「LINEを見て」と設定すれば、用事をすぐに把握することができます。

一方既製品は使用する音声は事前に用意されたメロディであることが多いです。
そうなってくると、「この曲はご飯、この曲はお風呂…」と慣れるまでに1週間はかかると思います。

呼び出しシステムの作り方!

では早速作っていきましょう。

  • スマホから呼び出せる
  • 面倒な設定なし
  • 自由に音声を設定できる

これらを満たすように作っていきます!

必要なもの

主に必要なパーツは、

  • Raspberry Pi Pico W
  • DFPlayer Mini
  • スピーカー
  • MicroSDカード

です。DFPlayer Miniは、マイコンボードから好きなmp3ファイルを再生するためのモジュールです。

Raspberry Pi Picoについて詳しく知りたい人は、下の入門記事を御覧ください!

配線

基本的な配線はこんな感じです。

配線元配線先
Raspberry Pi Pico GP12DFPlayer Mini RX
Raspberry Pi Pico GP13DFPlayer Mini TX
Raspberry Pi Pico 3.3V/GNDDFPlayer VCC/GND
スピーカーDFPlayer SPK1 – SPK2

プログラム

プログラムは次の2つのファイルを保存してください。

長いので折りたたんでいます

main.py
import network
import socket
import time
import ubinascii
from machine import UART, Pin, reset

from pages import home_page

SSID = "あなたのWi-Fi名"
PASSWORD = "あなたのWi-Fiパスワード"

STATIC_IP = "192.168.10.210"
SUBNET = "255.255.255.0"
GATEWAY = "192.168.10.1"
DNS = "8.8.8.8"

VOLUME = 22

uart = UART(0, baudrate=9600, tx=Pin(12), rx=Pin(13))
led = Pin("LED", Pin.OUT)

start_time = time.time()

FILES = {
    1: "お風呂入って",
    2: "降りてきて",
    3: "LINE見て",
}

def dfplayer_cmd(cmd, param=0):
    high = (param >> 8) & 0xFF
    low = param & 0xFF

    packet = bytearray([
        0x7E, 0xFF, 0x06, cmd, 0x00,
        high, low, 0x00, 0x00, 0xEF
    ])

    checksum = 0 - sum(packet[1:7])
    packet[7] = (checksum >> 8) & 0xFF
    packet[8] = checksum & 0xFF

    uart.write(packet)

def set_volume(volume):
    volume = max(0, min(30, volume))
    dfplayer_cmd(0x06, volume)

def play_file(file_num):
    led.on()
    set_volume(VOLUME)
    time.sleep(0.1)
    dfplayer_cmd(0x03, file_num)
    time.sleep(0.3)
    led.off()

def connect_wifi():
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)

    wlan.ifconfig((STATIC_IP, SUBNET, GATEWAY, DNS))
    wlan.connect(SSID, PASSWORD)

    print("Wi-Fi接続中...")

    while not wlan.isconnected():
        time.sleep(0.5)

    print("Wi-Fi接続完了")
    print("IP:", wlan.ifconfig()[0])

    mac = ubinascii.hexlify(wlan.config("mac"), ":").decode()
    print("MAC:", mac)

    return wlan

def ensure_wifi(wlan):
    if wlan.isconnected():
        return True

    print("Wi-Fiが切断されました。再接続します。")
    led.on()

    try:
        wlan.disconnect()
    except:
        pass

    time.sleep(1)
    wlan.connect(SSID, PASSWORD)

    for _ in range(20):
        if wlan.isconnected():
            print("Wi-Fi再接続成功")
            print("IP:", wlan.ifconfig()[0])
            led.off()
            return True
        time.sleep(0.5)

    print("Wi-Fi再接続失敗")
    led.off()
    return False

def send_response(client, body, content_type="text/html; charset=utf-8"):
    client.send("HTTP/1.1 200 OK\r\n")
    client.send("Content-Type: " + content_type + "\r\n")
    client.send("Connection: close\r\n\r\n")
    client.sendall(body)
    client.close()

def start_server(wlan):
    addr = socket.getaddrinfo("0.0.0.0", 80)[0][-1]

    s = socket.socket()
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(addr)
    s.listen(1)
    s.settimeout(2)

    print("Webサーバー起動")
    print("http://" + STATIC_IP)

    while True:
        ensure_wifi(wlan)

        try:
            client, addr = s.accept()
        except OSError:
            continue

        try:
            request = client.recv(1024).decode("utf-8")
            print(request)

            if "GET /api/play?file=" in request:
                file_text = request.split("GET /api/play?file=")[1].split(" ")[0]
                file_num = int(file_text)

                if file_num in FILES:
                    send_response(
                        client,
                        '{"ok": true, "file": %d}' % file_num,
                        "application/json; charset=utf-8"
                    )

                    play_file(file_num)
                    continue

                send_response(
                    client,
                    '{"ok": false, "error": "invalid_file"}',
                    "application/json; charset=utf-8"
                )
                continue

            if "GET /api/status" in request:
                uptime = int(time.time() - start_time)
                response = '{"ok": true, "status": "alive", "uptime": %d}' % uptime
                send_response(client, response, "application/json; charset=utf-8")
                continue

            send_response(client, home_page())

        except Exception as e:
            print("エラー:", e)

            try:
                send_response(
                    client,
                    '{"ok": false, "error": "server_error"}',
                    "application/json; charset=utf-8"
                )
            except:
                pass

wlan = connect_wifi()

time.sleep(1)
set_volume(VOLUME)

start_server(wlan)
pages.py
def home_page():
    return """<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>呼び出しボタン</title>
<style>
body {
  font-family: sans-serif;
  text-align: center;
  padding: 20px;
  background: #f7f7f7;
}

h1 {
  font-size: 32px;
  margin-bottom: 20px;
}

button {
  width: 95%;
  max-width: 500px;
  height: 110px;
  font-size: 34px;
  margin: 14px 0;
  border: none;
  border-radius: 24px;
  background: #1976d2;
  color: white;
}

button:disabled {
  background: #777;
}

#message {
  height: 40px;
  font-size: 24px;
  color: #333;
  margin-top: 20px;
}

.success {
  background: #2e7d32 !important;
}

.error {
  background: #c62828 !important;
}
</style>
</head>
<body>
<h1>呼び出し</h1>

<button onclick="play(1, this)">お風呂入って</button>
<button onclick="play(2, this)">降りてきて</button>
<button onclick="play(3, this)">LINE見て</button>

<p id="message"></p>

<script>
function play(file, button) {
  const message = document.getElementById("message");
  const originalText = button.innerText;

  button.disabled = true;
  button.innerText = "送信中...";
  message.innerText = "";

  fetch("/api/play?file=" + file)
    .then(response => {
      if (!response.ok) {
        throw new Error("送信失敗");
      }
      return response.json();
    })
    .then(data => {
      if (data.ok) {
        button.classList.add("success");
        button.innerText = "送信しました";
        message.innerText = "送信できました";
      } else {
        throw new Error("再生できませんでした");
      }
    })
    .catch(error => {
      button.classList.add("error");
      button.innerText = "失敗しました";
      message.innerText = "もう一度押してください";
    })
    .finally(() => {
      setTimeout(() => {
        button.disabled = false;
        button.classList.remove("success");
        button.classList.remove("error");
        button.innerText = originalText;
        message.innerText = "";
      }, 1500);
    });
}
</script>
</body>
</html>"""

以下の部分は使用しているWiFiの設定に合わせてください。

SSID = "あなたのWi-Fi名"
PASSWORD = "あなたのWi-Fiパスワード"

STATIC_IP = "192.168.10.210"
SUBNET = "255.255.255.0"
GATEWAY = "192.168.10.1"
DNS = "8.8.8.8"

呼び出し音声の用意

MP3ファイルをMicroSDカードに保存します。
保存するときは0001.mp3, 0002.mp3のように数字4桁に名前を変更してください。

ここで、音声はVOICEVOXなどの合成音声で作ってもいいのですが、僕は実際の家族の音声を録音しました。

呼び出しシステムの使い方

装置を電源に接続すると、main.pyが自動で起動します。

その後、STATIC_IPに設定したIPアドレスにブラウザでアクセスすると画像のような画面になります。
画面に表示されたボタンをタップすればスピーカーから音声が流れます。

yossy

ホーム画面に追加しておくと、すぐに開くことができるのでオススメです!

使ってみた

実際に使っている様子はこちら。(プログラムはテスト用なので若干異なります。)

まとめ:電子工作の利点は「自由さ」

今回は1階から2階にいる家族を呼び出せるシステムを作ってみました。

電子工作でスマート家電を自作することで、自分の環境にちょうどいい仕様にすることができます。これは、市販品にはできない電子工作ならではの利点ですね!

ということで、この記事が少しでも皆さんの参考になれば嬉しいです。

この記事が気に入ったら
いいねしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次