【電子工作】Arduinoで取得したセンサデータをPC経由でBluetoothでラズパイに飛ばす
今回の電子工作は「Arduinoに接続したCdSセンサのデータをPCで取得し、そのデータをBluetoothでラズパイに飛ばしてLEDの明るさをコントロールする」というものです。PCからラズパイにBluetoothでデータを飛ばしたものはこちら。
なぜこれをやろうと思ったか
前回の末尾で「脳波コントロールやるぞー」といっていましたが、見積とってみたら良いお値段でした。さすがにこれをテストで使ってみよう、とはなかなかいかず、まずは筋電センサを使ってみよう、となったわけです。比較的安価で使いやすそうな筋電センサだと、MyoWareのものになるわけなんですが、マニュアルをみると、これってArduinoでセンサデータを吸い上げる仕様になっているのです。
Arduino、家にあるんですが、Lチカやって満足してしまい、だいぶ長い間お蔵入りしていました。なんとなくArduinoには苦手意識があって、どうにかセンサから直接PCにデータを送れないか、いろいろ調べてみたらば、東京デバイセズの「マッスルリンク」というのがあるではないですか。
しかし、これはこれで良い値段しますし、複数個同時接続したらうまくデータがとれるんだろうか?とかあれこれ考えてしまい、だったらここは頑張ってArduinoを使いこなせるようになった方が良いのじゃなかろうか、と思い立ち、今回の電子工作に思い至ったわけです。
作ったものその1
Arduinoの回路
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); }
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
回路全体
次に、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をこねこねいじっているのはそういう理由です。