🏃‍♂️

Grove人感センサの値をUSB経由で取得

2024/03/10に公開

TL;DR

  • Grove対応USBアダプタを用いる
  • PythonライブラリPyMCP2221Aを用いる

背景

  • SBC等を使わずにPCで人感センサの値を取得したい
  • GPIO、I2C周りは詳しくない

内容

使用デバイス

Grove - PIR Motion Sensor - Seeed Studio

GroveをUSB経由で制御するアダプタ (みんなのラボ)

アダプタのGroveコネクタと人感センサをケーブルで接続する。
USBを任意のPCに接続する。(Windows10, Ubuntu22.04で動作確認)
電圧は手元の環境ではどちらでも動くことを確認。

取得用スクリプト

上記アダプタ開発者が紹介しているPythonライブラリPyMCP2221Aを用いる。

これはこのアダプタに特化したライブラリというわけではなく、アダプタに用いられているMCP2221AというUSBシリアル変換ICに対して値を取得できるようにしたライブラリのようである。

ただ、このライブラリのややイケていない点として初期化関数内でprint()が呼ばれているため強制的にライブラリから標準出力に書き出されてしまうということがある。

PyMCP2221A.py
    def Reset(self):
        print("Reset")
        buf = self.compile_packet([0x00, 0x70, 0xAB, 0xCD, 0xEF])

        self.mcp2221a.write(buf)
        time.sleep(1)

初期化関数のprint()以外の処理を抜き出して直接呼び出すことでこれを回避する。

他にも、サンプルスクリプトがいくつかあるが処理の仕様がどこにも明記されていないので何のためにやっているかわからない処理が多数あったり、VID, PID, devnumをメンバに持たせないようなクラスにしているのでリセット後の再認識のためにはオプション引数を省略せずに定義することが必須など、ライブラリとして扱うには色々と気になるところがある。

また、hidapiというライブラリも使うため依存関係として入っているが、もしインストールされなかったら以下でインストールする。

command
pip install hidapi

以下取得用サンプルスクリプト

sample.py
import time
from argparse import ArgumentParser, Namespace
from datetime import datetime
from typing import Callable

import hid
from PyMCP2221A.PyMCP2221A import PyMCP2221A

RESET_SIGNATURES: tuple[int, ...] = (0x00, 0x70, 0xAB, 0xCD, 0xEF)
RESET_MESSAGES: tuple[int, ...] = RESET_SIGNATURES + tuple(
    [0 for _ in range(65 - len(RESET_SIGNATURES))]
)
VID: int = 0x04D8
PID: int = 0x00DD


def main(args: Namespace) -> None:
    device: PyMCP2221A = PyMCP2221A(VID=VID, PID=PID, devnum=args.dev_num)
    device.mcp2221a.write(RESET_MESSAGES)
    time.sleep(1)
    while args.dev_num < len(hid.enumerate(VID, PID)) - 1:
        time.sleep(1)

    read_func: Callable[[], None] = None
    get_func: Callable[[int], int] = None

    if args.dev_mode == "a":
        print("mode: ADC")
        device.ADC_1_Init()
        device.ADC_2_Init()
        device.ADC_3_Init()
        read_func = device.ADC_DataRead

        def get_adc_data(i: int) -> int:
            return getattr(device, f"ADC_{i}_data")

        get_func = get_adc_data

    elif args.dev_mode == "g":
        print("mode: GPIO")
        device.GPIO_Init()
        device.GPIO_0_InputMode()
        device.GPIO_1_InputMode()
        device.GPIO_2_InputMode()
        device.GPIO_3_InputMode()
        read_func = device.GPIO_Read

        def get_gpio_data(i: int) -> int:
            return getattr(device, f"GPIO_{i}_INPUT")

        get_func = get_gpio_data

    else:
        raise ValueError(f"undefined mode: {args.dev_mode}")

    while True:
        print(datetime.now(), read_func.__name__)
        read_func()
        [print(get_func(i)) for i in range(2, 4)]
        time.sleep(1)


if __name__ == "__main__":
    parser: ArgumentParser = ArgumentParser()
    parser.add_argument("-d", "--dev_num", type=int, default=0)
    parser.add_argument("-m", "--dev_mode", choices=("a", "g"), default="a")
    main(parser.parse_args())

実行

command
python sample.py -d デバイス番号(複数接続している場合は整数で指定) -m g (g: GPIOモード。aもしくは未指定: ADCモード)

結果例

****-**-** **:**:**.****** GPIO_Read
85
83
****-**-** **:**:**.****** GPIO_Read
・・・
※センサ値は1回につき2つ(内部的に2つセンサがある?)。
スクリプトを止めるまで無限で値を取得し続ける。
スクリプト説明

デバイス初期化

冒頭のdevice.mcp2221a.write(RESET_MESSAGES)あたりの処理は以下からただ持ってきているだけである。何をしているかはよくわかっていない。time.sleep()での待機もあまりスマートではないように思える。

print()不使用。

デバイス認識

while args.dev_num < len(hid.enumerate(*DEV_ID)) - 1としているのは、手元の環境では直前の初期化処理を実行してからデバイスとして再認識されるまでに数十秒かかることがあったためである。
これを挟まないとデータ取得に失敗する。

ADC

アナログ値をデジタル値(0/1)に変換した値を取得したい場合はこちらの処理にする。
センサーが動きを感知していると判断している間は1、感知していないと判断している間は0となる。0/1の閾値はデバイス依存。
I/Fの初期化や取得関数はサンプルコードを参考。

GPIO

0/1に正規化される前のセンサ値の元データをそのまま取得したい場合はこちらの処理にする。
0~正の整数値を取るので、運用時には自分で閾値を設定して感知している/感知していないの条件を決める。
デバイスによって取り得る範囲は変わると思われるので実際に動かして確認する。

I/F共通

ADCの場合は1~3の3つI/FがあるがGROVEの人感センサーでは2と3のみを使用している模様。
GPIOの場合は0~3の4つI/FがありADCと同様2と3のみを使用している模様。
上記より、取得は2,3番目のI/Fの値を取り出すようにしている。

Discussion