はじめに
こんにちは!
本記事は、2024年 SPRESENSE™ 活用コンテストの応募記事です。
「Spresenseのサンプルもらえるぞ!」という仕事仲間の甘いささやきに乗って応募しました。
できたもの
ジャンプに合わせて♡が浮かび上がるトランポリンを作成しました。
制作過程
アイデア出し
娘(3歳)がダンスが好きなので音ゲー的なものを作りたいと考えていました。
DANCERUSH STARDOM的なものを作りたいと考えました。
そんな中、家にトランポリンがあったので、Spresenseとトランポリンを組み合わせて、近しいものを作りたいと考えました。
そこで考えたのが、インタラクティブなトランポリンです。
ジャンプすると♡マークが浮かび上がる魔法のトランポリンだよ!と娘には説明しています。
システム構成
加速度センサで、ジャンプを検出し、LEDテープを光らせます。
使用した部材は以下の通りです。
- 部材リスト
※1 メインボードと拡張ボードはコンテスト運営様よりご提供いただきました。この場を借りて感謝申し上げます。
ピンアサイン
ピンアサインは以下の通りです。
こちらのNeoPixelの仕様を見て決めています。
電源については、Neopixelが3.3Vでは足りないので、拡張ボードから5Vを引いています。
信号についても、メインボードだと基準電圧が足りないので、拡張ボードから引いています。
実装
実機はこんな感じになりました。
ユニバーサル基板を付けると、Addonボードで浮いてしまうので、Addonボード部分をくり抜いています。
基板が完成したので、構築していきます。
このケース、拡張ボード込みでほぼピッタリサイズです。
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); // センサー読み取り間隔
}
動作の様子
一斉に全部光らせるよりも、シーケンシャルに光らせる方がテンション上がりますね!
おわりに
娘も喜んでくれて、満足です!
次回は音楽と連携して、より音ゲーぽくしていきたいです。
-
zaki383
さんが
2025/01/31
に
編集
をしました。
(メッセージ: 初版)
-
zaki383
さんが
2025/01/31
に
編集
をしました。
ログインしてコメントを投稿する