【電子工作】Arduinoで取得したセンサデータをPC経由でBluetoothでラズパイに飛ばす

今回の電子工作は「Arduinoに接続したCdSセンサのデータをPCで取得し、そのデータをBluetoothでラズパイに飛ばしてLEDの明るさをコントロールする」というものです。PCからラズパイにBluetoothでデータを飛ばしたものはこちら。

wanko-sato.hatenablog.com

なぜこれをやろうと思ったか

前回の末尾で「脳波コントロールやるぞー」といっていましたが、見積とってみたら良いお値段でした。さすがにこれをテストで使ってみよう、とはなかなかいかず、まずは筋電センサを使ってみよう、となったわけです。比較的安価で使いやすそうな筋電センサだと、MyoWareのものになるわけなんですが、マニュアルをみると、これってArduinoでセンサデータを吸い上げる仕様になっているのです。

www.switch-science.com

Arduino、家にあるんですが、Lチカやって満足してしまい、だいぶ長い間お蔵入りしていました。なんとなくArduinoには苦手意識があって、どうにかセンサから直接PCにデータを送れないか、いろいろ調べてみたらば、東京デバイセズの「マッスルリンク」というのがあるではないですか。

tokyodevices.com

しかし、これはこれで良い値段しますし、複数個同時接続したらうまくデータがとれるんだろうか?とかあれこれ考えてしまい、だったらここは頑張ってArduinoを使いこなせるようになった方が良いのじゃなかろうか、と思い立ち、今回の電子工作に思い至ったわけです。

うだうだ考えずにやってみよう

そんなわけで、大きくは二つのことをやりました。

  1. ArduinoとPCを接続し、Arduinoからのセンサデータを取得する
  2. Arduinoから取得したデータをBluetoothでラズパイに送り、ラズパイ側でLEDの明るさをコントロールする

単純に「たまたま家にあったから」という理由でCdSセンサを使っています。

CdSセンサって?

ググればいろいろ出てくるのですが、「光可変抵抗器」という名の通り、光を当てると抵抗が変化するものです。光電効果を利用しており、暗いと抵抗が大きく、光を当てると抵抗が小さくなります。その性質を利用して明るくなったらor暗くなったら何かの動作をさせる、という用途で使われたりします。

作ったものその1

Arduinoの回路

f:id:wanko_sato:20220108140248p:plain

5Vピン-330Ω抵抗-CdSセンサ-GNDのラインと、5Vピン-330Ω抵抗-A0ピンのラインがあります。330Ω抵抗のところでCdSセンサにいくラインとA0ピンにいくラインに分かれているのがポイントです。
A0はアナログ入出力で、電圧をアナログ値として取得します。どういう値をとるかというと、

  • 暗いとき = CdSセンサの抵抗が大きいとき、5Vピンからの電流がA0ピンの方にのみ流れるため、大きな値をとります
  • 明るいとき = CdSセンサの抵抗が小さいとき、5Vピンからの電流がA0だけでなくCdSセンサ側も通れるため、小さな値をとります

ざっくりこんな仕様になっています。A0ピンにいくラインをCdSセンサの出口側に入れて直列回路にすると、上記の値の関係は逆になります。

Arduinoのコード

int val = 0;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
}

void loop() {
  // put your main code here, to run repeatedly:
  val = analogRead(0);
  Serial.println(val/4);
  delay(50);
}

Arduino IDEで書いて実行すればOK。

pythonのコード

import serial
import pygame
from matplotlib import pyplot as plt

# serial port open
print('COM port open')

# Arduion側のポート設定
# ポート名はコンパネのデバイスとプリンターで確認できる
s_a = serial.Serial('COM5', 9600, timeout=10)

# pygame window setting
BLACK = (0,0,0)
WHITE = (255, 255, 255)
WIDTH = 320
HEIGHT = 240

# setting for chart
MAX_LEN = 100
Y_AXIS = [0, 1]
X_INTERVAL = 0.05

'''
arduino側は特に実行せず、python側のコードのみ実行すれば良い
その際、シリアルモニタは切っておっこと。シリアルモニタがポートを占有してしまうため
ただし、IDEを立上げてコンパイルしておかないといけない
'''
def main():
	# variables for data showing
    pygame.init()
    surface = pygame.display.set_mode((WIDTH, HEIGHT))
    myfont = pygame.font.Font(None, 30)
    myclock = pygame.time.Clock()
    endflag = 0

    x = [0.0]
    y = [0.0]

    fig,ax = plt.subplots(figsize=(10, 6))
    lines, = ax.plot(x,y)

    # loop until showwindow close
    while endflag == 0:
		# ウィンドウが閉じられるまでwhileループを実行する
        for event in pygame.event.get():
            if event.type == pygame.QUIT: endflag = 1

        m = s_a.readline().decode('utf-8').replace('\r\n','')

        # まれにArduino側の送信がミスることがあるようなので
        # ValueErrorが発生したときは一瞬0を代入することにする
        try:
            m = float(m)
        except ValueError:
        	m = float(0)

        # X軸の幅を設定する
        # ウィンドウ幅が最大の長さより短い場合は0~最大長さとし、
        # 最大長さよりも長くなった場合は1つずつずらしてウィンドウ幅を維持する
        if len(x) < MAX_LEN:
            x.append(x[-1]+X_INTERVAL)
            x_lower = 0
            x_upper = X_INTERVAL * MAX_LEN
        else:
            x.append(x[-1]+X_INTERVAL)
            del x[0]
            x_lower = x[0]
            x_upper = x[-1]

        # プロットデータの配列長さを一定幅に維持する
        if len(y) < MAX_LEN:
            y.append(m/255)
        else:
            y.append(m/255)
            del y[0]

        # set_dataとplt.pauseを使用してpygameの描画遅延を回避
        lines.set_data(x, y)
        ax.set_xlim(x_lower, x_upper)
        ax.set_ylim((Y_AXIS[0], Y_AXIS[1]))
        plt.pause(0.001)

        # pygameで現在の値を表示
        surface.fill(WHITE)
        bitmaptext = myfont.render('resist:{}'.format(m/255), True, BLACK) # 今回の回路では明るいほど値が小さく、暗いほど値が大きくなる
        surface.blit(bitmaptext, (50, 75))
        pygame.display.update()
        myclock.tick(60)

    # whileループから抜けたらシリアル通信をcloseする
    s_a.close()
    pygame.quit()
    

if __name__ == '__main__':
	main()

Arduinoでコードを実行した後、pythonのコードをCMDなりPowerShellなりで実行すれば良い。
ちょっとだけ手の込んだことをやっていて、取得したデータをもとにグラフを動的に描画するようにしています。もしかしたらこれ、やりすぎるとメモリ食いつぶして停止、とかしてしまうかもなのですが、今後、筋電センサを使うことを想定し、どういうデータが時系列的に取得できるのか、見ておきたいと思ったわけなのです。

作ったものその2

回路全体

f:id:wanko_sato:20220108142359p:plain

次に、Arduinoからとったデータをラズパイに送ります。Arduino側は回路もプログラムも同じものを使います。
ラズパイ側は21ピンからLEDに入力し、220Ωの抵抗を挟んでGNDに送ります。

pythonコード

import serial
import pygame
from matplotlib import pyplot as plt

# serial port open
print('COM port open')

# Arduion側のポート設定
# ポート名はコンパネのデバイスとプリンターで確認できる
s_a = serial.Serial('COM5', 9600, timeout=10)

# ラズパイへの送信用
s_b = serial.Serial('COM3', 9600)

# pygame window setting
BLACK = (0,0,0)
WHITE = (255, 255, 255)
WIDTH = 320
HEIGHT = 240

# setting for chart
MAX_LEN = 100
Y_AXIS = [0, 1]
X_INTERVAL = 0.05

'''
arduino側は特に実行せず、python側のコードのみ実行すれば良い
その際、シリアルモニタは切っておっこと。シリアルモニタがポートを占有してしまうため
ただし、IDEを立上げてコンパイルしておかないといけない
'''
def main():
	# variables for data showing
    pygame.init()
    surface = pygame.display.set_mode((WIDTH, HEIGHT))
    myfont = pygame.font.Font(None, 30)
    myclock = pygame.time.Clock()
    endflag = 0

    x = [0.0]
    y = [0.0]

    fig,ax = plt.subplots(figsize=(10, 6))
    lines, = ax.plot(x,y)

    # loop until showwindow close
    while endflag == 0:
		# ウィンドウが閉じられるまでwhileループを実行する
        for event in pygame.event.get():
            if event.type == pygame.QUIT: endflag = 1

        m = s_a.readline().decode('utf-8').replace('\r\n','')

        # まれにArduino側の送信がミスることがあるようなので
        # ValueErrorが発生したときは一瞬0を代入することにする
        try:
            m = float(m)
        except ValueError:
        	m = float(0)

        # X軸の幅を設定する
        # ウィンドウ幅が最大の長さより短い場合は0~最大長さとし、
        # 最大長さよりも長くなった場合は1つずつずらしてウィンドウ幅を維持する
        if len(x) < MAX_LEN:
            x.append(x[-1]+X_INTERVAL)
            x_lower = 0
            x_upper = X_INTERVAL * MAX_LEN
        else:
            x.append(x[-1]+X_INTERVAL)
            del x[0]
            x_lower = x[0]
            x_upper = x[-1]

        # プロットデータの配列長さを一定幅に維持する
        if len(y) < MAX_LEN:
            y.append(m/255)
        else:
            y.append(m/255)
            del y[0]

        # set_dataとplt.pauseを使用してpygameの描画遅延を回避
        lines.set_data(x, y)
        ax.set_xlim(x_lower, x_upper)
        ax.set_ylim((Y_AXIS[0], Y_AXIS[1]))
        plt.pause(0.00001)

        # pygameで現在の値を表示
        surface.fill(WHITE)
        bitmaptext = myfont.render('resist:{}'.format(m/255), True, BLACK) # 今回の回路では明るいほど値が小さく、暗いほど値が大きくなる
        surface.blit(bitmaptext, (50, 75))
        pygame.display.update()
        myclock.tick(60)

        # ラズパイへの送信
        m = str(float(m)) + '\r\n'
        s_b.write(m.encode('utf-8'))

    # whileループから抜けたらシリアル通信をcloseする
    s_a.close()
    s_b.close()
    pygame.quit()
    

if __name__ == '__main__':
	main()

基本的にはその1と同じですが、Bluetooth接続したCOM3のラズパイにデータを送るためのs_bを新たに定義し、writeを使って送信しています。

ラズパイのpythonコード

import RPi.GPIO as GPIO
import serial
import pygame

s = serial.Serial('/dev/rfcomm0', 9600)

led_pin = 21

GPIO.setmode(GPIO.BCM)
GPIO.setup(led_pin, GPIO.OUT)

led = GPIO.PWM(led_pin, 50)
led.start(0)
bright = 0

flg = 0

BLACK = (0,0,0)
WHITE = (255, 255, 255)
WIDTH = 320
HEIGHT = 240

def main():
    pygame.init()
    surface = pygame.display.set_mode((WIDTH, HEIGHT))
    myfont = pygame.font.Font(None, 30)
    myclock = pygame.time.Clock()
    
    while flg == 0:
        for event in pygame.event.get():
            if event.type == pygame.QUIT: end == 1
        
        m = s.readline().decode('utf-8').replace('\r\n','')
        print(float(m))
        
        bright = (100 - (float(m)/255)*100) ** 2
        led.ChangeDutyCycle(bright)
        
        surface.fill(WHITE)
        bitmaptext_0 = myfont.render('bright:{}'.format(bright), True, BLACK)
   
        surface.blit(bitmaptext_0, (50, 75))
        
        pygame.display.update()
        
    pygame.quit()
    GPIO.cleanup()
        
if __name__ == '__main__':
    main()

21ピンに刺さったLEDに対してChangeDutyCycleで明るさの値を渡しています。なお、ChangeDutyCycleで受け取れる値は100が上限なので、もとの値を変換する必要があります。brightをこねこねいじっているのはそういう理由です。

動作手順

  1. PCとラズパイをBluetoothでペアリングする
  2. ラズパイのターミナルでリスン状態にする(前回記事を参照せよ)
  3. Arduinoのコードを実行
  4. PCのpythonコードを実行
  5. ラズパイのpythonコードを実行

という手順で実行すると、CdSセンサが受け取った明るさに応じてLEDの明るさが変化します。このコードだと直線的に変化しますが、brightの算出式を変更してもらえれば指数関数的に変化させることもできるはずです。

まとめ

これでArduino-PC-ラズパイの連携ができるようになりました。これで筋電センサも使えるはずです。
Bluetooth接続になっているのがまだ気になっていて、MQTT接続にできんかなぁ、と考えているところです。Herokuに無料のCloudMQTTがある、との情報を得て試しに登録してみたのですが、"out of stock"(在庫なし)となってしまい無料版は現在使えない状態のようです。ここは今後の課題ということで、今回は以上です。