zaki383 が 2025年01月31日00時11分18秒 に編集
初版
タイトルの変更
【SPRESENSE2024】ハートフル✨なトランポリン
タグの変更
SPRESENSE
Neopixel
加速度センサー
メイン画像の変更
記事種類の変更
製作品
ライセンスの変更
(MIT) The MIT License
本文の変更
# はじめに こんにちは! 本記事は、[2024年 SPRESENSE™ 活用コンテスト](https://elchika.com/promotion/spresense2024/)の応募記事です。 「Spresenseのサンプルもらえるぞ!」という仕事仲間の甘いささやきに乗って応募しました。 # できたもの ジャンプに合わせて♡が浮かび上がるトランポリンを作成しました。 @[x](https://x.com/zakuzaku_3/status/1884956202989424758) # 制作過程 ## アイデア出し 娘(3歳)がダンスが好きなので音ゲー的なものを作りたいと考えていました。 [DANCERUSH STARDOM](https://p.eagate.573.jp/game/dan/1st/entrance.html)的なものを作りたいと考えました。 そんな中、家にトランポリンがあったので、Spresenseとトランポリンを組み合わせて、近しいものを作りたいと考えました。 そこで考えたのが、インタラクティブなトランポリンです。 ジャンプすると♡マークが浮かび上がる魔法のトランポリンだよ!と娘には説明しています。 ## システム構成 全体的なシステム構成は下記の通りです。  加速度センサで、ジャンプを検出し、LEDテープを光らせます。 使用した部材は以下の通りです。 - 部材リスト - [Spresenseメインボード](https://developer.sony.com/ja/spresense/products/spresense-main-board)※1 - [Spresense 拡張ボード](https://developer.sony.com/ja/spresense/products/spresense-extension-board#buynow)※1 - [SONY SPRESENSE用6軸 加速度計・ジャイロスコープセンサ (BMI270) Addonボード ](https://www.switch-science.com/products/9870?variant=44031854379206) - [超極細 3535NeoPixel MiniテープLED 1m/60LED [9095]](https://www.akiba-led.jp/product/1883) - [DAISO コンサート用デコうちわ(ハート、ブラック)](https://jpbulk.daisonet.com/products/4550480262017) - [DAISO ミニビニールケース](https://jpbulk.daisonet.com/products/4550480454177?_pos=10&_sid=bfd5a59f2&_ss=r) - 適当なユニバーサル基板 - 拡張ボードに5V3A給電可能な電源 ※1 メインボードと拡張ボードはコンテスト運営様よりご提供いただきました。この場を借りて感謝申し上げます。 ## ピンアサイン ピンアサインは以下の通りです。 [こちら](https://www.tme.eu/Document/01c0100fee68667af99767edc3a7fee2/WS2812B-MINI.pdf)のNeoPixelの仕様を見て決めています。 電源については、Neopixelが3.3Vでは足りないので、拡張ボードから5Vを引いています。 信号についても、メインボードだと基準電圧が足りないので、拡張ボードから引いています。  ## 実装 実機はこんな感じになりました。  ユニバーサル基板を付けると、Addonボードで浮いてしまうので、Addonボード部分をくり抜いています。  LEDテープの端子をグルーガンで固定しました。 基板が完成したので、構築していきます。  DAISOで購入したうちわとケースです。  このケース、拡張ボード込みでほぼピッタリサイズです。 Spresenseを持ち運びするにはちょうどよいです。  うちわに貼り付けて完成です! 最後に、トランポリンに装着していきます。 1回、トランポリンの裏面に貼り付けようとしたのですが、相当な粘着力がないと厳しかったので、あきらめました。 そこで、クッションの上に置くことで、対応しました。 ローテクですが良い感じです。   ## ソースコード コードは[こちら](https://github.com/zakuzakuzaki/lightning-heart)に格納しています。 main.cppを以下に貼っておきます。 - ポイント - 加速度センサの値を取得するコードはAddonボードの販売者の[example](https://github.com/fooping-tech/Spresense_6dof_add_on/tree/main/example/Spresense_6dof_add_on_test)を利用しています。 - ジャンプのような躍動感を表現したかったので、シーケンシャルに光らせています。 - スレッドを利用し、ジャンプを検出したらシーケンシャルに光らせる関数を非同期で実行しています。 ```cpp:main.cpp #include <SpresenseNeoPixel.h> #include <BMI270_Arduino.h> #include <pthread.h> #include <unistd.h> const uint16_t PIN = 9; // NeoPixelの信号線用のデジタルピン番号 const uint16_t NUM_PIXELS = 60; // NeoPixelのLEDの数 SpresenseNeoPixel<PIN, NUM_PIXELS> neopixel; // IMUの設定 BMI270Class BMI270; // 加速度のしきい値 const float JUMP_THRESHOLD = 12.0; // Z軸加速度のしきい値 // 不感時間(ミリ秒単位) const unsigned long DEBOUNCE_TIME = 300; // ms unsigned long lastJumpTime = 0; // 最後にジャンプを検出した時間 // スレッドの引数用構造体 struct TaskParams { uint8_t r; uint8_t g; uint8_t b; uint8_t activeLEDCount; // 1回のジャンプで光らせるLEDの数 uint16_t speedDelay; // 光の移動速度(ミリ秒単位) }; // LEDの色のパレットを定義 const uint8_t colorPalette[][3] = { {255, 0, 0}, // 赤 {0, 255, 0}, // 緑 {0, 0, 255}, // 青 {255, 255, 0}, // 黄 {0, 255, 255}, // シアン {255, 0, 255}, // マゼンタ {255, 128, 0}, // オレンジ {128, 0, 255}, // パープル {0, 128, 255}, // 水色 {255, 192, 203}, // ピンク (ライトピンク) {255, 105, 180}, // ホットピンク {255, 182, 193} // ライトサーモンピンク }; // パレットのサイズ const size_t paletteSize = sizeof(colorPalette) / sizeof(colorPalette[0]); // カウンター変数(現在の色のインデックス) size_t currentIndex = 0; // 順番に虹色を取得 void getColor(uint8_t &r, uint8_t &g, uint8_t &b) { // 色を設定 r = colorPalette[currentIndex][0]; g = colorPalette[currentIndex][1]; b = colorPalette[currentIndex][2]; // インデックスを次に進める(範囲を超えたらリセット) currentIndex = (currentIndex + 1) % paletteSize; } // ハート型の中心から外側に光るシーケンス(非同期タスク) void *lightUpHeartTask(void *parameters) { TaskParams *params = (TaskParams *)parameters; uint8_t r = params->r; uint8_t g = params->g; uint8_t b = params->b; uint8_t activeLEDCount = params->activeLEDCount; uint16_t speedDelay = params->speedDelay; // LEDが60連結しているが、♡の中央上部から下部に向かってシーケンシャルに光らせたいので、LEDテープの真ん中から光らせる。 uint8_t heartPattern[30][2] = { {29, 30}, {28, 31}, {27, 32}, {26, 33}, {25, 34}, {24, 35}, {23, 36}, {22, 37}, {21, 38}, {20, 39}, {19, 40}, {18, 41}, {17, 42}, {16, 43}, {15, 44}, {14, 45}, {13, 46}, {12, 47}, {11, 48}, {10, 49}, {9, 50}, {8, 51}, {7, 52}, {6, 53}, {5, 54}, {4, 55}, {3, 56}, {2, 57}, {1, 58}, {0, 59}}; // 光る動作(上部から下部に広がる) for (int i = 0; i < 30 + activeLEDCount; i++) { for (int j = 0; j < activeLEDCount; j++) { int index = i - j; // 現在の位置から遡って点灯 if (index >= 0 && index < 30) { neopixel.set(heartPattern[index][0], r, g, b); neopixel.set(heartPattern[index][1], r, g, b); } } // 消灯する処理 if (i >= activeLEDCount) { int indexToTurnOff = i - activeLEDCount; if (indexToTurnOff < 30) { neopixel.set(heartPattern[indexToTurnOff][0], 0, 0, 0); neopixel.set(heartPattern[indexToTurnOff][1], 0, 0, 0); } } neopixel.show(); // 光の移動速度を調整 usleep(speedDelay * 1000); } // 最後の残りを消灯 neopixel.set(heartPattern[29][0], 0, 0, 0); neopixel.set(heartPattern[29][1], 0, 0, 0); neopixel.show(); delete params; return NULL; } void setup() { // デバッグ用にシリアル通信を開始 Serial.begin(115200); neopixel.clear(); // IMUの初期化 int8_t rslt = BMI270.begin(BMI270_I2C, BMI2_I2C_SEC_ADDR); BMI270.IMU_print_rslt(rslt); rslt = BMI270.IMU_configure_sensor(); BMI270.IMU_print_rslt(rslt); Serial.println("Setup complete"); } void loop() { struct bmi2_sens_float sensor_data; int8_t rslt = BMI270.bmi2_get_sensor_float(&sensor_data); if (rslt == BMI2_OK) { float IMU_z = sensor_data.acc.z; Serial.printf("IMU_z: %f\n", IMU_z); // 現在の時刻を取得 unsigned long currentTime = millis(); // ジャンプ検出条件と不感時間の確認 if (IMU_z > JUMP_THRESHOLD && (currentTime - lastJumpTime > DEBOUNCE_TIME)) { // 最後にジャンプを検出した時間を更新 lastJumpTime = currentTime; uint8_t r, g, b; getColor(r, g, b); // スレッドパラメータの設定 TaskParams *params = new TaskParams{r, g, b, 5, 1}; // 非同期スレッドの起動 pthread_t thread; pthread_create(&thread, NULL, lightUpHeartTask, params); pthread_detach(thread); // メインスレッドから独立して実行 } } delay(10); // センサー読み取り間隔 } ``` ## 動作の様子 @[x](https://x.com/zakuzaku_3/status/1884957165758423257) 一斉に全部光らせるよりも、シーケンシャルに光らせる方がテンション上がりますね! # おわりに 娘も喜んでくれて、満足です! 次回は音楽と連携して、より音ゲーぽくしていきたいです。