ブラウザ上で物理スイッチの入力状態を表示したい

技術

はじめに

先日、Raspberry Pi Model B (4GB)を購入しました。1万円ほどで買える安価なコンピュータです。パソコンとして使うだけでなく、電子部品をピンに繋いで電子工作することもできます。

この記事では、Raspberry Piに物理的なスイッチを繋いで、スイッチの押下状態リアルタイムで出力しようと思います。WebSocketを使ったブラウザ上での表示も実現できましたので備忘録として残しておきます。

部品調達

スイッチやワイヤーなど、必要なものを揃えます。

電子部品は秋月電子通商で購入するのが良いかと思います。筆者は秋葉原の秋月電子通商に行きました。

まずは、物理的なスイッチです。1個30円なので結構安いです。
種類はいくつかありますのでお好きなスイッチをお選びください。

トグルスイッチ(秋月電子通商のページより、画像をクリックすると秋月電子通商のページに遷移します)

次にスイッチを取り付けるブレッドボードです。

ブレッドボード(秋月電子通商のページより、画像をクリックすると秋月電子通商のページに遷移します)

画像ではボタンが大きく見えていますが、実際にはブレッドボードの穴3×3程度でとても小さいです。

そして、Raspberry Piとスイッチを接続するケーブルです。10本入りですが、使うのは2本。

ジャンパーワイヤー(秋月電子通商のページより、画像をクリックすると秋月電子通商のページに遷移します)

Raspberry Piに部品を接続

スイッチをブレッドボードに取り付けます。スイッチが置けるところならどこでも大丈夫です。見栄えが良いのは、ブレッドボードの中央かと思います。
筆者はボタンの足をブレッドボードの(d14, g14, d16, g16)に挿しています。

ワイヤーを取り付けます。ボタンを取り付けた位置によって変わりますが、ブレッドボードの14行目と16行目のどこでも良いのでワイヤーを挿します。筆者はa14とa16に挿しています。

ピン配置(Raspberry Pi 公式ドキュメントより)

Raspberry Piの公式ドキュメントを参考に、ワイヤーのもう片方をRaspberry Piの右側上から6番目GPIO18と、右側上から7番目Groundにそれぞれ接続します。
Raspberry PiのGroundと書いてある箇所ならどこでも大丈夫ですが、隣同士だと間違いが少ないので、ここではGPIO18の隣としています。

Raspberry Piと部品を接続

上の画像ではRaspberry PiのGroundに接続しているピンが上で説明した位置とは異なります。

ターミナル上でスイッチの入力状態を表示

まずはターミナル上でスイッチの状態を表示させます。

やることはこちらのページと大体一緒です。

Raspberry Pi上のターミナルを開きます。どこでもいいので、Pythonファイルを作成します。ここではデスクトップにswitch.pyというファイルを作成しています。

$ cd ~/Desktop
$ touch switch.py

switch.pyの内容は、次のようにします。

import time

# GPIOの初期設定
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)

# GPIO18を入力端子設定
GPIO.setup(18, GPIO.IN, pull_up_down=GPIO.PUD_UP)

while True:
    # スイッチ状態取得
    sw_status = GPIO.input(18)

    # 画面出力
    if sw_status == 0:
        print('ON')
    else:
        print('OFF')

    # 少し待つ
    time.sleep(0.3)

GPIOの入力ですが、通電しているときは0通電していないときは1を返すという仕様のようです。なんだか直感に反していますね。

ターミナル上でPythonファイルを実行します。

$ python switch.py

スイッチをポチポチして遊んでみます。ボタンを押しているときにON、離すとOFFがターミナル上に出力されたかと思います。

Djangoを利用してブラウザ上でスイッチの入力状態を表示

ターミナルで表示させると、
ON ON ON OFF OFF…
ずら〜っと表示されていて見栄えはあまり良くありません。

ブラウザ上で表示できるようにします。スイッチの押下状態を表示する部分を用意して、状態が変わった場合にその部分を書き換えて表示させることとします。ついでに、何回押したかも表示させます。

使用するのはDjangoというフレームワークです。大げさすぎるかもしれませんが、テンプレートのHTMLファイルを用意すれば流用できるってだけで採用しています。

いや、この例だけでは大げさすぎるな。

一般的にはpipenvなどで仮想環境を作って、その中にDjangoや必要なモジュールをインストールします。筆者も最初はRaspberry Pi上に仮想環境を作って、その中でDjangoを動かしていました。しかし、Raspberry PiにはRPi.GPIOという特殊なモジュールがデフォルトで入っていて、仮想環境にこのモジュールをインストールしてもエラーが発生して、動作しませんでしたので実環境で進めます。

Djangoをインストールします。

$ pip install Django

Djangoのプロジェクトを作成します。ここではデスクトップにGPIOというプロジェクトを作成しています。

$ cd ~/Desktop
$ python -m django startproject GPIO
$ cd GPIO

GPIOディレクトリ内にswitchというアプリケーションを作成します。

$ python -m django startapp switch

GPIO/GPIO/settings.pyを開きます。
ALLOWED_HOSTが書かれている行を探して、次のように変更します。

ALLOWED_HOSTS = ["raspberrypi.local", "0.0.0.0"]

Raspberry Piのホスト名をデフォルトのraspberrypiから変えている場合は、適宜ご自身で設定したホスト名に変更してください。

サーバを起動して、うまく動作していることを確認します。

$ python manage.py runserver 0.0.0.0:8000

ブラウザで、http://0.0.0.0:8000 または、http://raspberrypi.localにアクセスします。

Raspberry Piのホスト名をデフォルトのraspberrypiから変えている場合は、適宜ご自身で設定したホスト名に変更してください。

The install worked successfully! 🚀などと表示されていればOKです。

ターミナルに戻って、[Ctrl] + [C]でサーバを止めます。

続いて、WebSocketを実現するchannels, channels_redis、ASGIサーバのdaphneをインストールします。

$ pip install channels channels_redis daphne

また、本記事執筆時点での各モジュールのバージョンは次のようになっています。

channels: 4.0.0
channels-redis: 4.1.0
daphne: 4.0.0
Django: 4.2.4

GPIO/GPIO/settings.pyで、INSTALLED_APPSの配列に以下を追加します。

INSTALLED_APPS = [
    "daphne",
    "switch.apps.SwitchConfig",
    # "channels",

    ### 省略 ###
]

daphneを一番上にしないと動かないかもしれません。channelsは書かなくても動作しましたのでコメントアウトしてます。

続いて、GPIO/switchにswitch.pyというファイルを作成します。ファイルの内容は次のようにします。

# GPIOの初期設定
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)

# GPIO18を入力端子設定
GPIO.setup(18, GPIO.IN, pull_up_down=GPIO.PUD_UP)

count = 0
flag = False


def read_sensor_data():
    global count, flag
    i = GPIO.input(18)
    if i == 0 and flag == False:
        # チャタリング防止
        count += 1
        flag = not flag
    elif i == 1 and flag:
        flag = not flag
    return [i, count]

続いて、GPIO/switchにconsumers.pyというファイルを作成します。ファイルの内容は次のようにします。

import json
import asyncio
from channels.generic.websocket import AsyncWebsocketConsumer
from .switch import read_sensor_data


class SensorDataConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        print("websocket: connected")
        await self.accept()
        while True:
            sensor_data = read_sensor_data()
            send_text = "OFF"
            if sensor_data[0] == 0:
                send_text = "ON"
            await self.send_sensor_data([send_text, sensor_data[1]])
            try:
                await asyncio.sleep(0.1)  # 適切な間隔を設定
            except asyncio.exceptions.CancelledError as e:
                break

    async def send_sensor_data(self, data):
        await self.send(text_data=json.dumps({'sensor_data': data[0], "count": data[1]}))

    async def websocket_disconnect(self, message):
        return await super().websocket_disconnect(message)

    async def disconnect(self, code):
        return await super().disconnect(code)

connect関数はブラウザからサーバに接続できたときに実行されます。0.1秒ごとにスイッチの押下状態と押された回数を送信し続けます。

続いて、GPIO/GPIO/asgi.pyを開きます。ファイルの内容をすべて削除して、次のように変更します。

from switch import consumers
from django.urls import path
from channels.routing import ProtocolTypeRouter, URLRouter
import os

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Test.settings')


application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": URLRouter([
        path("ws/sensor_data/", consumers.SensorDataConsumer.as_asgi()),
    ]),
})

GPIO/GPIO/settings.pyを開きます。
末尾に次のコードを追加してください。

ASGI_APPLICATION = 'GPIO.asgi.application'

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("raspberrypi.local", 6379)],
        },
    },
}

Raspberry Piのホスト名をデフォルトのraspberrypiから変えている場合は、適宜ご自身で設定したホスト名に変更してください。

同じくsettings.pyのTEMPLATESと書かれた配列を次のように変更します。

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            os.path.join(BASE_DIR, "templates"),
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

‘DIR’の部分を変更しています。

また、osモジュールをsettings.pyのインポートに追加します。

from pathlib import Path
import os # 追加

templatesディレクトリを追加します。場所はプロジェクトルートディレクトリのGPIO内です。GPIO/GPIO内ではありませんのでご注意ください。

templatesディレクトリ内にindex.htmlを作成します。

{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-HwwvtgBNo3bZJJLYd8oVXjrBZt8cqVSpeBNS5n7C8IVInixGAoxmnlMuBnhbgrkm"
        crossorigin="anonymous"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <title>Switch</title>

    <style type="text/css">

    </style>
    <script language="javascript" type="text/javascript">

    </script>
</head>

<body>
    {% if messages %}
    <div class="alert alert-success" role="alert">
        <strong>{% trans "Messages:" %}</strong>
        <ul>
            {% for message in messages %}
            <li>{{message}}</li>
            {% endfor %}
        </ul>
    </div>
    {% endif %}

    <script>
        const socket = new WebSocket('ws://' + window.location.host + '/ws/sensor_data/');

        socket.onmessage = function (e) {
            const data = JSON.parse(e.data);
            const sensorData = data.sensor_data;
            const count = data.count;
            // console.log(data)

            // センサーデータを表示する処理を記述
            // 例: document.getElementById('sensor-data').innerHTML = sensorData;
            $('.python_data').text(sensorData + " (" + count + "回目)")
        };
    </script>
    <div class="p-3 p-sm-5 mb-4 text-center" style="background-color: #e9ecef;">
        <div class="container">
            <h1 class="display-5">Switch</h1>
            <p class="python_data">{{ now }}</p>
        </div>
    </div>
    <br>


    {% block extra_body %}
    {% endblock %}
</body>

</html>

ページを開いたときにサーバと接続します。サーバから送られたメッセージを受信した際に、メッセージを扱いやすい形式にして取り出しています。

GPIO/switch/views.pyを編集します。

from django.shortcuts import render
from django.views import generic


class TopView(generic.TemplateView):
    template_name = 'index.html'

    def get(self, request, *args, **kwargs):
        context = super(TopView, self).get_context_data()
        return render(request, self.template_name, context)
    pass

GPIO/GPIO/urls.pyを編集します。

from django.contrib import admin
from django.urls import path
import switch.views as views

urlpatterns = [
    path('admin/', admin.site.urls),
    path("", views.TopView.as_view(), name="top"),
]

ここまでで、ディレクトリ構成が次のようになっているはずです。
水色:編集するファイル
橙色:追加するファイル

GPIO
├── GPIO
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-39.pyc
│   │   ├── asgi.cpython-39.pyc
│   │   ├── settings.cpython-39.pyc
│   │   ├── urls.cpython-39.pyc
│   │   └── wsgi.cpython-39.pyc
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── __pycache__
│   └── manage.cpython-39.pyc
├── db.sqlite3
├── manage.py
├── switch
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-39.pyc
│   │   ├── admin.cpython-39.pyc
│   │   ├── apps.cpython-39.pyc
│   │   ├── consumers.cpython-39.pyc
│   │   ├── models.cpython-39.pyc
│   │   ├── switch.cpython-39.pyc
│   │   └── views.cpython-39.pyc
│   ├── admin.py
│   ├── apps.py
│   ├── consumers.py
│   ├── migrations
│   │   ├── __init__.py
│   │   └── __pycache__
│   │       └── __init__.cpython-39.pyc
│   ├── models.py
│   ├── switch.py
│   ├── tests.py
│   └── views.py
└── templates
    └── index.html

8 directories, 31 files

Raspberry PiのGPIOにピンを接続します。先ほどと同じように取り付けてください。

サーバを起動して、動作を確認します。

$ python manage.py runserver 0.0.0.0:8000

ブラウザで、http://0.0.0.0:8000 または、http://raspberrypi.localにアクセスします。

Raspberry Piのホスト名をデフォルトのraspberrypiから変えている場合は、適宜ご自身で設定したホスト名に変更してください。

スイッチをポチポチ押すと、スイッチの状態と何回押したかが表示されたかと思います。

同じネットワーク内の別の端末からアクセスすると、サーバを起動してから何回スイッチを押したかが表示されるかと思います。

おわりに

ターミナルでスイッチの状態を出力するのも悪くはありませんが、単調で面白みがありません。そこで今回は、ブラウザに表示させるようにしました。

ブラウザ上で表示させることができると、同一のネットワーク内の別の端末から見ることができ、シンクロして表示されます。

加速度センサを取り付けたRaspberry Piを車に積んで車速を確認するなど、各種センサを取り付けたRaspberry Piをブラウザ経由で情報を確認するといったことに応用できそうです。

コメント

タイトルとURLをコピーしました