Ami_Nitoのアイコン画像
Ami_Nito 2024年10月31日作成 (2024年10月31日更新)
製作品 製作品 閲覧数 81
Ami_Nito 2024年10月31日作成 (2024年10月31日更新) 製作品 製作品 閲覧数 81

トリックオアトリート!!

トリックオアトリート!!

1. プロジェクト概要

  • プロジェクトの目的と背景
    • 「目的」
      ハロウィンを子どもたちと一緒に楽しみたい!

    • 「背景」
      ハロウィンでお菓子とぬいぐるみを使って子どもたちに楽しんでもらいたい。
      また、お家でハロウィンパーティーするときのひとネタとして楽しんでもらいたいと思った。

2. コンセプトとデザイン

  • コンセプトの説明
    「お菓子泥棒検知」機能を持つハロウィンロボット。子どもたちがハロウィンで楽しむお菓子を守りながら、インタラクティブに遊べる体験を提供する。
    パンダおばけの右目には赤外線人感センサーが設置されている。誰かがお菓子に近づくと自動的に反応し、「トリックオアトリート!トリックオアトリート!」と声を発し、左手を上げて挨拶をする。もしお菓子が取られたら、パンダおばけが「お菓子泥棒!お菓子泥棒!お菓子をくれなきゃいたずらするぞ!」と声を発し、赤色のLEDが点滅する。その後、お菓子泥棒を把握するべく、写真撮影をしてcloudinaryへアップロードする。もしお菓子をあげたら、「ありがとう!いたずらしない!」と声を発し、3色のLEDが順番に点灯する。

  • デザインの概要
    見た目は、かわいくて少し不気味なパンダおばけにし、ハロウィンのムードを
    盛り上げられるように装飾した。また、パンダの前に台(重量測定センサー)
    を設置し、その上に入れ物を置き、その入れ物をパンダが抱えるようにして
    お菓子を守っているように見せている。
    ラズベリーパイや電池、配線等はぬいぐるみの中に入れ込み機械感が出な
    いようにした。

3. ハードウェアの構成

  • ハードウェア要件
    ・Raspberry Pi 3B:各種センサーやモニターを制御する
    ・赤外線人感センサー(HC-SR501):人を検知し、トリガーをかける
    ・サーボモーター(SG90):ぬいぐるみの腕を上下に動かす
    ・ロードセルキット(HX711他):お菓子の量をリアルタイムで測定
    ・USBカメラ(OV7670):お菓子が減ったときに写真を撮影
    ・AUX充電式スピーカー(LC001 3W):音声ファイルを再生してぬいぐるみが喋るようにする
    ・フレキシブルLED(CY-FRX3002-M2001シリーズ):お菓子の増減によって点滅
    ・電源ユニット:Raspberry Piの電力供給
    ・配線、コネクタ、抵抗:各部品の接続
    ・電池ホルダー:LED電源用
    ・単三電池:LED電源用
    ・PNPトランジスタ:LED制御用
    ・その他(ぬいぐるみ、固定具、装飾品等)

  • 回路図と配置図
    キャプションを入力できます

4. ソフトウェアの構成

  • ソフトウェア要件
    ・「人の検知とアクション」
    人を検知するとサーボモーターでぬいぐるみの腕を上げ、挨拶をする。
    腕を下げる動作も含めたサイクルを自動化。
    ・「重量の監視」
    人を検知したらバケツの重量をチェック。増減に応じて音声ファイルを再生。
    お菓子が入れられたとき→感謝のメッセージ、LED点灯
    お菓子が減ったとき→怒りのメッセージ、LED点滅、写真撮影とアップロード

  • プログラムのフローチャート
    キャプションを入力できます

5. 制作プロセス

  • プロジェクトのスケジュール
    ・計画立案:2日
    ・使用する物品の調査:2日
    ・物品購入:1日
    ・ハード組み立て:3日
    ・ソフト作成:1週間
    ・テスト:1週間
    ・資料作成:1日

  • 各フェーズの詳細

    • 【プロジェクトの計画】

      • 目的の設定:子ども向けの「お菓子泥棒検知ロボット」を作成することを目的とした。
      • 要件の定義:赤外線人感センサー、サーボモーター、重量センサー、カメラ、スピーカー、LEDを制御機器として使用することとした。
    • 【設計】

      • ハードウェア設計:部品の選定と回路図の作成を行った。
      • ソフトウェア設計:Pythonを使用して、各部品を制御するプログラムのフローチャートを作成した。
    • 【部品の調達】
      必要な電子部品をAmazonや秋月電子、千石電商にて購入。

    • 【プロトタイプの作成】

      • ハードウェアの組み立て:Raspberry Piにセンサーやアクチュエーターを接続し、回路を構築。
      • ソフトウェアの実装:
        ・赤外線人感センサー:motion.py
        ・ロードセルセンサー:weight.py
        ・サーボモーター:motor_talk.py
        ・音声:talk.pyとmotor_talk.py
        ・LED:led.py
        ・全体統括:main.py
    • 【テストとデバック】

      • ハードウェアテスト:各種センサーとアクチュエーターが正しく動作するか確認した。
      • ソフトウェアテスト:プログラムの各部分が期待通りに動作するか確認し、バグ修正を行った。処理が重なってしまうところは処理の順序やロック処理を入れて改善した。
    • 【最適化と改良】

      • パフォーマンスの最適化:重量測定の精度向上や音声再生の同期を実現するための改善を行った。
      • ユーザビリティの向上:子どもたちが楽しめるように、LEDの点灯や数パターンの音声再生を入れるようにした。Raspberry Pi のデフォルトでは低めの男性の声なので、OpenJtalkを使用し、女性の高い声になるようにした。
        ・使用したサイト:http://open-jtalk.sp.nitech.ac.jp/index.php
    • 【最終テストと評価】
      全体の動作を再確認し、細かい調整を行った。子どもたちからフィードバックをもらい、プロジェクト評価を行った。LEDが数色光るようにしてほしい、可愛い声にしてほしい等。

6. 課題と解決策

  • 直面した課題
    人の検知処理と重量測定処理が重なり、サーボモーターの動作や音声再生が干渉することがあった。

  • 解決策
    処理が重ならないように、片方の処理動作中はもう片方の処理を一時停止させるロック機能を追加した。

7. プログラムコード

motion.py

import RPi.GPIO as GPIO # GPIOモジュールインポート import time # timeモジュールインポート import motor_talk # motor_talk.pyインポート # GPIOの設定 PIR_PIN = 27 # 人感センサーを接続するGPIOピン番号 def setup_gpio(): # GPIOのモード設定 GPIO.setmode(GPIO.BCM) # BCMモードを設定 GPIO.setup(PIR_PIN, GPIO.IN) # 指定したピンを入力モードに設定 def detect_motion(): setup_gpio() # 同じスクリプト内のsetup_gpio関数の呼び出し print("PIRセンサーを使った動きの活発さを測定します。") try: if GPIO.input(PIR_PIN): # PIR_PINがTrueの場合以下処理実行 print("動きを検出しました!") motor_talk.move_servo() # motor_talk.pyのmove_servo関数の実行 motion_started = True # motion_started変数にTrueを代入(後にmotion.detect_motion()にブール値を返すため) else: print("動きなし") motion_started = False # motion_started変数にFalseを代入(後にmotion.detect_motion()にブール値を返すため) time.sleep(0.5) # センサーの状態を0.5秒ごとにチェック return motion_started # 呼び出し元であるmain.pyのmotion.detect_motion()にブール値を返す except KeyboardInterrupt: # Ctrl+Cが押された場合の例外処理 print("プログラムを終了します。") if __name__ == "__main__": detect_motion() # detect_motion関数の実行 GPIO.cleanup() # GPIOのクリーンアップ print("GPIOをクリーンアップしました。")

talk.py

import subprocess def play_audio(audio_file): """指定された音声ファイルを再生する""" subprocess.run(["aplay", audio_file]) # aplayコマンドで音声を再生

motor_talk.py

import RPi.GPIO as GPIO # GPIOモジュールインポート import time # timeモジュールインポート import talk # talk.pyインポート import weight # weight.pyインポート Servo_pin1 = 12 # サーボモータに接続するGPIOピン番号 audio_file_path = "/home/pi/dev/IoT_contest/20241028090436_4380.wav" # 再生する音声ファイルのパス # PWMの設定 Servo1 = None # Servo1に値がない、初期化状態である def setup_gpio(): # GPIOをセットアップする global Servo1 # 関数の外で定義された変数Servo1を関数内で変更したいためglobalで定義 if Servo1 is None: # すでに初期化されていない場合のみ初期化 GPIO.setmode(GPIO.BCM) # GPIOピンをBCM番号に指定する(デフォルトでは物理ピン番号モードが使用される) GPIO.setup(Servo_pin1, GPIO.OUT) # 指定したピンを出力モードに設定 Servo1 = GPIO.PWM(Servo_pin1, 50) # 50Hzで動作するサーボモーターの制御を準備 Servo1.start(0) # PWM信号を0%に設定し、サーボモーターを停止 def servo_angle(servo, angle): # サーボモーターの角度を設定 duty = 2.5 + (angle / 180) * 10 # サーボモーターの動作に必要なデューティサイクル(0°から180°までの角度を持ち、デューティサイクルは2.5%から12.5%の範囲)を計算 servo.ChangeDutyCycle(duty) # サーボモーターを指定した角度に動かす time.sleep(1) # 1秒間待機 servo.ChangeDutyCycle(0) # PWM信号を停止 def move_servo(): # サーボモーターを動かし、音声を再生する setup_gpio() # setup_gpio関数を呼び出す(GPIOの初期化) try: print("サーボモーターを180°に設定します...") servo_angle(Servo1, 180) time.sleep(1) print("サーボモーターを0°に動かします...") servo_angle(Servo1, 0) time.sleep(1) # サーボモーターの動作後に音声を再生 print("音声を再生します...") talk.play_audio(audio_file_path) move_servo_back() # move_servo_back関数を呼び出してサーボモーターを元の位置に戻し # サーボモーターの動作が終了した後に重量測定を行う weight.measure_weight() # weight.pyのmeasure_weight関数を呼び出す except KeyboardInterrupt: print("プログラムを終了します。") def move_servo_back(): # サーボモーターを180°に戻す print("サーボモーターを元に戻します...") servo_angle(Servo1, 180) # 180°に戻すためにservo_angle関数を呼び出す def cleanup_specific_pin(): """特定のピンをクリーンアップする関数""" GPIO.setup(Servo_pin1, GPIO.IN) # ピンを入力モードに設定して解放 if __name__ == "__main__": move_servo() # move_servo関数を呼び出す

weight.py

import RPi.GPIO as GPIO # GPIOモジュールインポート import time # timeモジュールインポート import threading # threadingモジュールインポート import hx711 # hx711モジュールインポート import talk # talk.pyインポート import led # led.pyインポート import take_picture # take_picture.pyインポート import upload_to_cloudinary # upload_to_cloudinary.pyインポート # GPIOピンの設定(DTとSCKピンを定義) DT_PIN = 5 # データピン SCK_PIN = 6 # クロックピン hx = hx711.HX711(DT_PIN, SCK_PIN) # HX711のインスタンスを作成 # 校正のための設定 referenceUnit = 200 # 校正の基準値の設定 hx.set_reference_unit(referenceUnit) # set_reference_unit(referenceUnit)メソッドを呼び出し、HX711が測定する値を特定の重量にマッピング hx.reset() # HX711をリセットして初期化 time.sleep(2) hx.tare() # 現在のセンサーの出力値をゼロ点として設定 tolerance = 6 # 許容誤差範囲(±6g) max_tolerance_count = 8 # 誤差範囲内での最大カウント数(のちのループ処理に使用) tolerance_count = 0 # 現在の誤差範囲内のカウント # 初期基準値の設定 initial_weight = hx.get_weight(5) # HX711センサーからの測定で、5回の測定を行い、その平均値を計算 print(f"初期基準重量: {initial_weight} g") # 平均値を表示 # スレッド制御用のフラグとロック is_action_running = False # LED点灯&音声再生中かどうか(Falseはなにも作動していない状態) lock = threading.Lock() # 処理の排他制御用ロック def play_audio_and_blink_led(file_path, led_function): # 音声ファイルを再生し、LEDを点滅させる関数 """ 音声を再生し、指定されたLED点灯関数を同時に実行。 音声とLEDの処理が完了するまで is_action_runningをTrueに保つ。 """ global is_action_running # 関数の外で定義された変数is_action_runningを関数内で変更したいためglobalで定義 with lock: # 他のスレッドがplay_audio_and_blink_led関数に同時にアクセスすることを防ぐ is_action_running = True # 処理開始のためTrueに切り替える led_thread = threading.Thread(target=led_function) # 指定されたLED点滅関数(led_function)を別スレッドで実行するためのスレッドを作成 led_thread.start() # LED点灯スレッドの開始 talk.play_audio(file_path) # 音声再生 led_thread.join() # LED点灯(led_thread)が完了するまで待機 is_action_running = False # 処理完了のためFalseに切り替える def measure_weight(): """ 重量測定を行い、重量の変化に応じて音声再生とLED点滅を制御。 誤差範囲内が一定回数続くとループを終了する。 """ global initial_weight global tolerance_count # 誤差範囲内のカウントを使用する for _ in range(20): # 最大15回の測定。各ループ内で重量を取得し、変更があれば処理を実行 current_weight = hx.get_weight(5) # 現在の重量を5回測定し、その平均値をcurrent_weightに代入 print(f"現在の重量: {current_weight} g") # 平均値を表示 weight_difference = current_weight - initial_weight # 基準重量との差を計算しweight_differenceに代入 if not is_action_running: # is_action_runningがFalseの場合(何も作動していない状態)のみ、以下の処理実行 if -tolerance <= weight_difference <= tolerance: # 重量が誤差範囲内(±tolerance)の場合は、処理を行わずカウントを増加 print("誤差範囲内のため、何もしません。") tolerance_count += 1 # 誤差範囲内のカウント else: tolerance_count = 0 # 誤差範囲を外れたらカウントをリセット if current_weight < initial_weight: # 現在の重量が基準重量を下回ったら以下処理実行 print("重量が減少しました!") # 減少を検知すると、play_audio_and_blink_led 関数で音声再生とLED点滅を別スレッドで開始 threading.Thread(target=play_audio_and_blink_led, args=("/home/pi/dev/IoT_contest/20241028090300_4364.wav", led.blink_led_16)).start() initial_weight = current_weight # 基準重量を現在の重量に更新 take_picture.take_picture() # take_picture.pyのtake_picture関数の実行 upload_to_cloudinary.upload_photo() #upload_to_cloudinary.pyのupload_photos関数の実行 elif current_weight > initial_weight: # 現在の重量が基準重量を上回ったら以下処理実行 print("重量が増加しました!") # 増加を検知すると、play_audio_and_blink_led 関数で音声再生とLED点滅を別スレッドで開始 threading.Thread(target=play_audio_and_blink_led, args=("/home/pi/dev/IoT_contest/20241028085653_4380.wav", led.blink_led)).start() initial_weight = current_weight # 基準重量を現在の重量に更新 else: # is_action_runningがTrueの場合以下処理実行 print("現在、音声再生とLED点灯が実行中です。次の測定を待機します。") if tolerance_count >= max_tolerance_count: # 誤差範囲内が最大カウント数に達したらループを抜ける print("誤差範囲内が続いたため、測定を終了します。") break time.sleep(1) # センサーの状態を1秒ごとにチェック if __name__ == "__main__": try: measure_weight() # measure_weight関数の実行 except Exception as e: # measure_weight()の実行中にエラーが発生した場合、エラーメッセージを表示 print(f"エラーが発生しました: {e}") finally: GPIO.cleanup() # プログラム終了時にGPIOをクリーンアップ

led.py

import RPi.GPIO as GPIO import time # GPIOの設定 GPIO.setmode(GPIO.BCM) # BCMピン番号方式を使用 GPIO.setwarnings(False) # GPIOの警告を無視 # 出力ピンの設定 led_pins = [16, 20, 21] # 使用するLEDのGPIOピン番号 for pin in led_pins: GPIO.setup(pin, GPIO.OUT) # 各ピンを出力ピンとして設定 def blink_led(repeat_count=1, interval=0.5): try: for _ in range(repeat_count): # LEDを点灯 for pin in led_pins: GPIO.output(pin, GPIO.HIGH) # LEDをオン time.sleep(interval) # 指定された間隔待つ # LEDを消灯 for pin in led_pins: GPIO.output(pin, GPIO.LOW) # LEDをオフ time.sleep(interval) # 指定された間隔待つ except KeyboardInterrupt: pass def blink_led_16(repeat_count=3, interval=0.5): """ GPIO 16ピンのみを指定回数点滅させる関数 """ try: GPIO.setup(16, GPIO.OUT) # GPIO 16ピンを出力モードに設定 for _ in range(repeat_count): GPIO.output(16, GPIO.HIGH) # 16ピンをオン time.sleep(interval) # 指定された間隔待つ GPIO.output(16, GPIO.LOW) # 16ピンをオフ time.sleep(interval) # 指定された間隔待つ except KeyboardInterrupt: pass if __name__ == "__main__": blink_led() # すべてのLEDを点滅 blink_led_16() # 16ピンのみ点滅 GPIO.cleanup() # GPIOの設定をクリア

take_picture.py

import os import subprocess from datetime import datetime def take_picture(): # 保存するディレクトリのパス image_folder = "/home/pi/dev/IoT_contest/image" # ディレクトリが存在しない場合は作成 if not os.path.exists(image_folder): os.makedirs(image_folder) # 現在の日時を取得し、フォーマットを指定 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") photo_path = os.path.join(image_folder, f"photo_{timestamp}.jpg") # 写真を撮影して保存 command = f"fswebcam -r 640x480 --jpeg 85 -D 1 {photo_path}" # コマンドを実行 try: subprocess.run(command, shell=True, check=True) print(f"写真が撮影されました: {photo_path}") except subprocess.CalledProcessError as e: print("写真の撮影中にエラーが発生しました:", e) if __name__ == "__main__": take_picture()

upload_to_cloudinary.py

import os import cloudinary import cloudinary.uploader # Cloudinaryの設定 cloudinary.config( cloud_name='○○○', # CloudinaryのCloud Name api_key='○○○○', # API Key api_secret='○○○○' # API Secret ) # 画像の保存フォルダ image_folder = "/home/pi/dev/IoT_contest/image" # フォルダ内の最新の写真を取得 def get_photo(): photos = [os.path.join(image_folder, f) for f in os.listdir(image_folder) if f.endswith(".jpg")] if not photos: return None # 最新のファイルを取得 latest_photo = max(photos, key=os.path.getmtime) return latest_photo def upload_photo(): latest_photo = get_photo() if latest_photo: print(f"写真をアップロードします: {latest_photo}") try: # Cloudinaryに画像をアップロード response = cloudinary.uploader.upload(latest_photo) print("Uploaded Image URL:", response['url']) # アップロード成功後にローカルの画像を削除 os.remove(latest_photo) print(f"ローカルフォルダから削除されました: {latest_photo}") except cloudinary.exceptions.Error as e: # アップロード失敗時のエラーハンドリング print(f"アップロードに失敗しました: {e}") else: print("アップロードする最新の写真が見つかりません。") if __name__ == "__main__": upload_photo()

main.py

import RPi.GPIO as GPIO # GPIOモジュールインポート import time # timeモジュールインポート import threading # threadingモジュールインポート import motion # motion.pyインポート import motor_talk # motor_talk.pyインポート import weight # weight.pyインポート stop_event = threading.Event() # スレッド停止用のフラグ weight_measurement_started = False # 重量測定が開始されたかどうかのフラグ(Falseは開始されていない状態) servo_moved = False # サーボモーターが動作したかどうかのフラグ(Falseは開始されていない状態) def detect_monitor(): """ 人感センサーの監視と動作処理 """ global weight_measurement_started, servo_moved # 関数の外で定義された変数weight_measurement_started, servo_movedを関数内で変更したいためglobalで定義 # stop_eventがセットされていない(is_set()がFalse)間はループを続け、Trueで入ってきたらスレッドを停止 while not stop_event.is_set(): # is_set() は現在のフラグ状態を確認するためのメソッド if motion.detect_motion(): # 人が検知された場合(motion.pyのdetect_motionに入る) print("人が検知されました。") # 人が検知されたら重量測定を開始する if not weight_measurement_started: # Falseだったら(if not:何もしていない状態ではない場合)以下処理実行 print("重量測定を開始します。") weight_measurement_started = True # 重量測定を開始しましたというフラグに切り替える # もし、人が検知され続けていれば"重量測定を開始します。"というメッセージは表示されない。(フラグをTrueに切り替えたため) # 同じく、人が検知されたらサーボモーターが動作する if not servo_moved: # Falseだったら(if not:何もしていない状態ではない場合)以下処理実行 motor_talk.move_servo() # サーボモーターを動かす関数を呼び出す servo_moved = True # サーボモーターが動作したフラグを立てる else: # 人が検知されなくなった場合の処理 if weight_measurement_started: # weight_measurement_started = Trueだったら以下処理実行 print("人が検知されなくなりました。重量測定を停止します。") weight_measurement_started = False # 重量測定を停止 servo_moved = False # サーボモーターの動作フラグをリセット time.sleep(1) # 一定の間隔を空けて監視 def main_sleep(): # stop_eventがセットされていない(is_set()がFalse)間はループを続ける while not stop_event.is_set(): # is_set()は現在のフラグ状態を確認するためのメソッド time.sleep(1) print("main sleep") if __name__ == "__main__": # スクリプトが直接実行されたときにのみ以下を実行 try: # 人感センサースレッドを開始 detect_thread = threading.Thread(target=detect_monitor) # detect_monitor関数を実行するスレッド detect_thread.daemon = True # メインプログラムが終了した際にこのスレッドも自動的に終了 detect_thread.start() # スレッドを開始 main_sleep() # main_sleep関数を実行 except KeyboardInterrupt: # Ctrl+Cが押された場合の例外処理 print("Ctrl+Cが押されました。プログラムを終了します。") stop_event.set() # スレッドを停止させるフラグを立てる finally: # tryブロックが正常に実行された場合でも、except処理が発生した場合でも必ず実行される GPIO.cleanup() # プログラム終了時にGPIOをクリーンアップ print("GPIOをクリーンアップしました。")
ログインしてコメントを投稿する