pico w を使ったキーボード無線化の挑戦

先日アラサーBPさんが公開された pico w を使ったキーボード無線化に挑戦

twitter.com

qiita.com

購入したもの

販売コード 商品名 メモ
118021 Raspberry Pi Pico W ベーシックセット ピンも欲しかったのでベーシックセットを購入。不要な人は単体で買うと良い。
109833 006P安定化電源モジュールキット(5V出力) 降圧タイプ
103257 006Pアルカリ電池(積層電池) 9V ゴールデンパワー 5V以上のもの
100452 バッテリースナップ(電池スナップ・Bスナップ) 縦型 ソフトタイプ

他、picow(usb-micro-b) からキーボードに接続する USB ケーブルを用意しておくこと。私の場合は pico w ←→ usb-bオスusb-aメス ←→ usb-aオスusb-bオス ←→ キーボード で接続。

pico w のセットアップ

マスストレージにアラサーBPさんが公開しているファイル picow_ble_hid_keyboard.uf2 を入れるだけ。

github.com

作成物

全体像

アルカリ電池を 5V に降圧し、picow の VBUS,GND に接続。pico w の usb-micro-b はキーボードに接続。

Raspberry Pi Pico W

安定化電源モジュール

アルカリ電池

接続方法

安定化電源モジュールのスイッチを ON すると bluetooth がペアリングモードになるのでデバイスの追加を行う。以上。

バイスの追加

私的トラブル

私の環境ではデバイスの追加後、一度 OFF にして再度 ON にすると bluetooth の再接続を繰り返す現象が発生。PC に bluetooth が内蔵されていないため子機を繋げていたが、どうもその子機との相性が悪いらしい。別商品の子機に変えたら問題なく、また bluetooth 内臓のサブ機でも問題なかった。

自分でコンパイルする場合

アラサーBPさんの記事でも紹介されている Raspberry Pi Pico をセットアップしよう を参考にすると良い。「4.5. UART で「Hello World」テキストを表示する」までクリアできるとコンパイルから動作確認まで可能。

注意点として、Raspberry Pi 5 を使う場合は UART Port が変わっているので注意。 以下のどちらかで対応。

カレンダーからリマインダーに自動登録

iPhoneのおはなし

特定の日にやるべき作業をカレンダーに入れても忘れることがあるため、それをリマインダーに自動登録するショートカットを作成。

ショートカットの作成

  • カレンダーを以下の条件で検索

    • 開始日が今日
    • タイトルに「リマインド」を含む
  • ヒットしたカレンダー変数を繰り返しリマインダーに追加

オートメーションの設定

毎日 0:00 に実行するよう設定

カレンダーに予定の追加

タイトルに「リマインド」を入れる。

リマインダーに自動追加される

Raspberry Pi Zero WHを使用したSwitchBotデータの収集とグラフ作成 その2

前回の続き hemus.hatenablog.com

開発

プログラム改修

要点

  • cron で毎時 0, 15, 30, 45 分に実行するよう変更

  • sheet1 にデータを蓄積

    最終行に追加する手間がかかるため3行目に行追加するよう変更。2行目は数式が埋め込んであるためあえて3行目にしている。

  • sheet2 に最新のデータを登録

cron

0,15,30,45 * * * * /root/develop/run.sh

python

#!/usr/bin/python3

import sys
import datetime
import logging

# Switch-Botからデータ取得のためのインポート
import binascii
from bluepy.btle import Scanner, DefaultDelegate, BTLEDisconnectError

# GoogleSpreadSheetに書き込むためのインポート
import requests
import gspread
from oauth2client.service_account import ServiceAccountCredentials

logging.basicConfig(level=logging.ERROR)

# スプレッドシート
key_name = '{ jsonファイルを指定 }'
book_name = '{ Spreadsheet のファイル名 }'
sheet_name = '{ Spredsheet のシート名 }'
sheet_name2 = '{ Spredsheet のシート名2 }'

# GoogleAPIを通してスプレッドシートに接続
scope = ['https://spreadsheets.google.com/feeds','https://www.googleapis.com/auth/drive']
credentials = ServiceAccountCredentials.from_json_keyfile_name(key_name, scope)
gc = gspread.authorize(credentials)

# シートを取得
worksheet = gc.open(book_name).worksheet(sheet_name)
worksheet2 = gc.open(book_name).worksheet(sheet_name2)

class ScanDelegate(DefaultDelegate):
    def __init__(self, addr, name, no):
        DefaultDelegate.__init__(self)
        self.addr = addr
        self.name = name
        self.no = no
        self.processed_devices = set()  # 処理済みのデバイスの初期化

    def handleDiscovery(self, dev, isNewDev, isNewData):
        # アドレスチェック
        if dev.addr != self.addr:
            return

        # 処理済みでないかチェック
        if dev.addr in self.processed_devices:
            return

        # 主処理
        for (adtype, desc, value) in dev.getScanData():
            # 気温、湿度を読み込んでスプレッドシートに登録
            if(adtype == 22):
                servicedata = binascii.unhexlify(value[4:])
                battery = servicedata[2] & 0b01111111

                #print(self.name + "," + timestamp + "," + str(battery) + "," + str(temp) + "," + str(humid))
                self.writeSpreadsheet(self.name, timestamp, battery, temp,
                                      humid, self.no)

                # 処理したデバイスのアドレスを記録
                self.processed_devices.add(dev.addr)

                return
            elif(adtype == 255):
                madata = binascii.unhexlify(value[16:])
                humid = madata[4] & 0b01111111
                temp = (madata[2] & 0b00001111) / 10 + (madata[3] & 0b01111111)
                isOverZero=(madata[3]&0b10000000)
                if not isOverZero:
                    temp = -temp
                continue
            else :
                continue

    def writeSpreadsheet(self, name, timestamp, battery, temp, humid, no):
        # 書き込み
        # 2行目は表示書式、数式などを埋め込むためあえて3行目に追加する
        worksheet.insert_row([name, timestamp, temp, humid], 3)

        # 書き込み
        worksheet2.update_cell(1 + no, 1, name)
        worksheet2.update_cell(1 + no, 2, timestamp)
        worksheet2.update_cell(1 + no, 3, battery)
        worksheet2.update_cell(1 + no, 4, temp)
        worksheet2.update_cell(1 + no, 5, humid)

def scan(addr, name, no):
    scanner = Scanner().withDelegate(ScanDelegate(addr, name, no))
    try:
        # 指定秒BLEのスキャンを行う
        scanner.scan(20)
    except BTLEDisconnectError as e:
        pass
    except Exception as e:
        logging.error("Error during scanning: %s", e)

def main():
    global timestamp
    timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:00")

    scan("xx:xx:xx:xx:xx:x1", "名称1")
    scan("xx:xx:xx:xx:xx:x2", "名称2")
    scan("xx:xx:xx:xx:xx:x3", "名称3")

if __name__ == "__main__":
    main()

sheet1

蓄積データ。列 E,F,G はグラフ表示用に数式を設定。

sheet2

最新データ

グラフの作成

蓄積データについては気温、湿度それぞれでピボットテーブルを作ってグラフを作成する。

最新データも同様。

グラフのサイズを良い感じにし、インタラクティブに公開できるので設定、iframe を取得する。

Google ChartsをQuickにStartする  https://www.i-ryo.com/entry/2018/12/10/073520
「スプレッドシートのグラフは簡単に埋め込みできる」の項目を採用。
「Google ChartsをQuickにStart」の項目は、複数のチャートを表示することができず諦めた。

html の作成

取得した iframe を配置し、キャッシュなし、自動リロード設定。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <meta http-equiv="Pragma" content="no-cache">
    <meta http-equiv="Cache-Control" content="no-cache">
    <meta http-equiv="refresh" content="300">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>SwitchBot Meter</title>
</head>
<body style="background-color:#2c2f38;">

<iframe width="299" height="106" seamless frameborder="0" scrolling="no" src="hogehoge"></iframe>
~省略~

</body>
</html>

完成

Raspberry Pi Zero WHを使用したSwitchBotデータの収集とグラフ作成

目的

SwitchBot の気温や湿度を Windows 上に表示する。

環境

概要

  • SwitchBot ハブは持っていないため SwitchBot API は使わず Bluetooth(BLE) 接続で端末から直接取得する。
  • データの取得は Raspi zero で一定時間ごとに行い、取得したデータは Google スプレッドシートに記録する。
  • Google スプレッドシートのデータをグラフ化し、デスクトップアプリのような形で表示する。

開発

Raspberry Pi Zero WH の Bluetooth 確認

# hciconfig
ci0:   Type: Primary  Bus: UART
    BD Address: B8:27:EB:7D:69:B4  ACL MTU: 1021:8  SCO MTU: 64:1
    UP RUNNING
    RX bytes:3610 acl:0 sco:0 events:256 errors:0
    TX bytes:33188 acl:0 sco:0 commands:256 errors:0

"UP RUNNING" なので OK

Python 関連のインストール

pip3 に切り替えるタイミングってどこが良いのだろう。

# apt-get update
# apt-get install libbluetooth-dev libglib2.0-dev libboost-python-dev libboost-thread-dev
# apt-get install python3-pip
# apt-get install python3-gattlib
# apt-get install python3.11-venv
# python3 -m venv myenv
# source myenv/bin/activate
# pip3 install bluepy
# pip3 install gspread
# pip3 install oauth2client

PythonBluetooth 検知

SwitchBot アプリから確認できる MAC アドレスが実行結果に含まれていることを確認。 アドレスタイプが random なので電池切れなどの端末再起動で MAC アドレス変わるかも。

参考

SwitchBot 温湿度計の値を取得

どうも温湿度計(液晶あり)の方法では防水温湿度計(液晶なし)はうまくいかないらしい。

参考

SwitchBot 防水温湿度計の値を取得

バッテリー残量、温度、湿度が取得できたのを確認。

参考

PythonGoogle スプレッドシートに書き込み

SwitchBot からデータ取得、スプレッドシートに書き込みまでするプログラム。

#!/usr/bin/python3

import sys
import datetime
import logging

# Switch-Botからデータ取得のためのインポート
import binascii
from bluepy.btle import Scanner, DefaultDelegate, BTLEDisconnectError

# GoogleSpreadSheetに書き込むためのインポート
import requests
import gspread
from oauth2client.service_account import ServiceAccountCredentials

logging.basicConfig(level=logging.ERROR)

class ScanDelegate(DefaultDelegate):
    def __init__(self, addr, name):
        DefaultDelegate.__init__(self)
        self.addr = addr
        self.name = name
        self.processed_devices = set()  # 処理済みのデバイスの初期化

    def handleDiscovery(self, dev, isNewDev, isNewData):
        # アドレスチェック
        if dev.addr != self.addr:
            return

        # 処理済みでないかチェック
        if dev.addr in self.processed_devices:
            return

        # 主処理
        for (adtype, desc, value) in dev.getScanData():
            # 気温、湿度を読み込んでスプレッドシートに登録
            if(adtype == 22):
                servicedata = binascii.unhexlify(value[4:])
                battery = servicedata[2] & 0b01111111

                timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                #print(self.name + "," + timestamp + "," + str(battery) + "," + str(temp) + "," + str(humid))
                self.writeSpreadsheet(self.name, timestamp, battery, temp, humid)

                # 処理したデバイスのアドレスを記録
                self.processed_devices.add(dev.addr)

                return
            elif(adtype == 255):
                madata = binascii.unhexlify(value[16:])
                humid = madata[4] & 0b01111111
                temp = (madata[2] & 0b00001111) / 10 + (madata[3] & 0b01111111)
                isOverZero=(madata[3]&0b10000000)
                if not isOverZero:
                    temp = -temp
                continue
            else :
                continue

    def writeSpreadsheet(self, name, timestamp, battery, temp, humid):
        # スプレッドシート
        key_name = '{ jsonファイルを指定 }'
        book_name = '{ Spreadsheet のファイル名 }'
        sheet_name = '{ Spredsheet のシート名 }'

        # GoogleAPIを通してスプレッドシートに接続
        scope = ['https://spreadsheets.google.com/feeds','https://www.googleapis.com/auth/drive']
        credentials = ServiceAccountCredentials.from_json_keyfile_name(key_name, scope)
        gc = gspread.authorize(credentials)

        # シートを取得
        worksheet = gc.open(book_name).worksheet(sheet_name)

        # A列のデータを配列として取得
        A_COL_ARRAY = worksheet.col_values(1)

        # 最下行インデックスを取得
        LAST_ROW_IDX = len(A_COL_ARRAY)

        # 書き込み
        worksheet.update_cell(LAST_ROW_IDX+1, 1, name)
        worksheet.update_cell(LAST_ROW_IDX+1, 2, timestamp)
        worksheet.update_cell(LAST_ROW_IDX+1, 3, battery)
        worksheet.update_cell(LAST_ROW_IDX+1, 4, temp)
        worksheet.update_cell(LAST_ROW_IDX+1, 5, humid)

def scan(addr, name):
    scanner = Scanner().withDelegate(ScanDelegate(addr, name))
    try:
        # 指定秒BLEのスキャンを行う
        scanner.scan(20)
    except BTLEDisconnectError as e:
        pass
    except Exception as e:
        logging.error("Error during scanning: %s", e)

def main():
    scan("xx:xx:xx:xx:xx:x1", "名称1")
    scan("xx:xx:xx:xx:xx:x2", "名称2")
    scan("xx:xx:xx:xx:xx:x3", "名称3")

if __name__ == "__main__":
    main()

参考

cron 設定

python(pip) を cron で設定するは面倒らしいので、実行部分は shell で実装。ついでにログをファイル出力。

#!/bin/bash
source /hoge/myenv/bin/activate
python /hoge/switchbot_meter.py >> /hoge/output.log 2>> /hoge/error.log

crontab で 15 分毎の設定。

*/15 * * * * /hoge/run.sh

グラフの作成

Google スプレッドシートのデータを容易に扱える Looker Studio を使って折れ線グラフなどを作成。

15 分ごとの更新設定をしてるけどうまくいかず。

デスクトップ表示

Chrome の機能を使ってウェブサイトを疑似的にアプリ化。

参考

最後に

おおよその目的は達成できて満足。15 分ごとの更新ができていないが、raspi 上で php を動かしているのでそちらでグラフ表示&自動更新を考えている。(横軸が変わる場合に自動更新ができないというのをどこかで見た)

また、何も考えずデータの登録をしてしまったが、グラフ表示のことも考えてデータ登録を考えた方が良さそう。正規化大事。

  • 現在

    場所 タイムスタンプ バッテリー(%) 気温(℃) 湿度(%)
    ベランダ 2024-03-20 19:00:12 96 7.7 51
    ベビールーム 2024-03-20 19:00:30 92 22.7 33
    寝室 2024-03-20 19:00:48 100 13.8 44
  • 改善後(想定)

    場所 タイプ タイムスタンプ
    ベランダ バッテリー(%) 96 2024/3/20 19:00
    ベランダ 気温(℃) 7.7 2024/3/20 19:00
    ベランダ 湿度(%) 51 2024/3/20 19:00
    ベビールーム バッテリー(%) 92 2024/3/20 19:00
    ベビールーム 気温(℃) 22.7 2024/3/20 19:00
    ベビールーム 湿度(%) 33 2024/3/20 19:00
    寝室 バッテリー(%) 100 2024/3/20 19:00
    寝室 気温(℃) 13.8 2024/3/20 19:00
    寝室 湿度(%) 44 2024/3/20 19:00

愛用キーボード cocot46 plus

ここ1年半ほど愛用しているcocot46 plus

cocot46 plus

ケース

https://twitter.com/Hemus_/status/1549336438692806656 www.thingiverse.com

トラックボールの土台安定&光漏れ削減

https://twitter.com/Hemus_/status/1548506180397694976 www.thingiverse.com

40mmトラックボール

www.thingiverse.com

LEDにマステ

トラックボールの軌跡の調整 https://twitter.com/Hemus_/status/1546110418275680256

遊舎工房さんで購入できます https://shop.yushakobo.jp/products/6955