zaki383のアイコン画像
zaki383 2025年01月31日作成 (2025年01月31日更新) © MIT
製作品 製作品 閲覧数 195
zaki383 2025年01月31日作成 (2025年01月31日更新) © MIT 製作品 製作品 閲覧数 195

【SPRESENSE2024】ハートフル✨なトランポリン

はじめに

こんにちは!
本記事は、2024年 SPRESENSE™ 活用コンテストの応募記事です。
「Spresenseのサンプルもらえるぞ!」という仕事仲間の甘いささやきに乗って応募しました。

できたもの

ジャンプに合わせて♡が浮かび上がるトランポリンを作成しました。

制作過程

アイデア出し

娘(3歳)がダンスが好きなので音ゲー的なものを作りたいと考えていました。
DANCERUSH STARDOM的なものを作りたいと考えました。
そんな中、家にトランポリンがあったので、Spresenseとトランポリンを組み合わせて、近しいものを作りたいと考えました。
そこで考えたのが、インタラクティブなトランポリンです。
ジャンプすると♡マークが浮かび上がる魔法のトランポリンだよ!と娘には説明しています。

システム構成

全体的なシステム構成は下記の通りです。
キャプションを入力できます

加速度センサで、ジャンプを検出し、LEDテープを光らせます。

使用した部材は以下の通りです。

※1 メインボードと拡張ボードはコンテスト運営様よりご提供いただきました。この場を借りて感謝申し上げます。

ピンアサイン

ピンアサインは以下の通りです。
こちらのNeoPixelの仕様を見て決めています。
電源については、Neopixelが3.3Vでは足りないので、拡張ボードから5Vを引いています。
信号についても、メインボードだと基準電圧が足りないので、拡張ボードから引いています。
ピンアサイン

実装

実機はこんな感じになりました。
キャプションを入力できます
ユニバーサル基板を付けると、Addonボードで浮いてしまうので、Addonボード部分をくり抜いています。

キャプションを入力できます
LEDテープの端子をグルーガンで固定しました。

基板が完成したので、構築していきます。

キャプションを入力できます
DAISOで購入したうちわとケースです。

キャプションを入力できます
このケース、拡張ボード込みでほぼピッタリサイズです。
Spresenseを持ち運びするにはちょうどよいです。

キャプションを入力できます
うちわに貼り付けて完成です!

最後に、トランポリンに装着していきます。
1回、トランポリンの裏面に貼り付けようとしたのですが、相当な粘着力がないと厳しかったので、あきらめました。
そこで、クッションの上に置くことで、対応しました。
ローテクですが良い感じです。

キャプションを入力できます

キャプションを入力できます

ソースコード

コードはこちらに格納しています。
main.cppを以下に貼っておきます。

  • ポイント
    • 加速度センサの値を取得するコードはAddonボードの販売者のexampleを利用しています。
    • ジャンプのような躍動感を表現したかったので、シーケンシャルに光らせています。
    • スレッドを利用し、ジャンプを検出したらシーケンシャルに光らせる関数を非同期で実行しています。

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); // センサー読み取り間隔 }

動作の様子

一斉に全部光らせるよりも、シーケンシャルに光らせる方がテンション上がりますね!

おわりに

娘も喜んでくれて、満足です!
次回は音楽と連携して、より音ゲーぽくしていきたいです。

1
ログインしてコメントを投稿する