AtomS3Rを使ってコイントスとダイスロール機能作ってみた
成果物
まずはデモ動画をご覧ください!
こんな感じでAtomS3Rを使って簡易的なコイントスとダイスロール機能を作ってみました!
似たようなものを作りたい人は参考にしてみてください!
必要なもの
必須
- AtomS3R(本体):スイッチサイエンス
- 手元に
AtomS3がないためそちらで動くかは未検証ですが、
M5Unifiedライブラリを使っているためそのままか微調整で動作すると思います。
- 手元に
- パソコン(AtomS3Rへの書き込みに必要です。)
- USBケーブル(パソコンとAtomS3Rをつなげるために必要です。)
任意
- ATOMIC バッテリーベース:スイッチサイエンス
※AtomS3R単体(ケーブルレス)で動作させたい場合は欲しいです。
はじめに
冒頭に見せた機能を最初から作ろうと思っていたわけではなく、
AtomS3Rを衝動で買ったあとに何を作ろうと思って、まずは仕様を調べるところから始めました。
仕様的に気にしたのは以下のあたりです。
特徴
- プログラム可能なボタン
仕様
- カラー IPS 解像度:128 × 128
- 6 軸姿勢センサ (BMI270):精度:0.05 % (加速度)、0.05 °/s (角速度) / I2C アドレス:0x68
- 3 軸地磁気センサ (BMM150):精度:0.3 µT / BMI270 に実装され、BMI270 から磁気データを取得
- 赤外線 IR:∠180° 時、障害物なしで 12.46 m まで送信可能
これらの仕様を踏まえて以下のようなことを考えました。
- ボタンがあるため、ボタンを押した時という判定が使えそう。
- 6軸姿勢センサ(加速度センサ×ジャイロセンサ)があるため、自身が動いた判定や、自身の向きを判定に使えそう。
- 3軸地磁気センサがあるため、方位推定(コンパス)に使えそう。
- 何かのアウトプットとして赤外線送信が出来そう。
- 何かのアウトプットとして液晶に表示が出来そう。
これらのことから今回は以下の2機能を作ってみようと思いました。
- コイントス
- ダイスロール
環境構築
作るものが決まったら次は環境構築です。
以前Arduino IDEは使ったことがあったので、今回はそちらで実装しました。
公式のセットアップ手順を元に、Arduino IDEの環境を作ります。
- Arduino IDE のインストール
- Arduino ボード管理
- Arduino ライブラリ管理
の3つの手順を行いました。
3. Arduino ライブラリ管理ではM5Unifiedをインストールしました。
※環境構築の詳しい手順自体は公式手順そのままだったため省きますが、
2. Arduino ボード管理の1. インストールボードの管理にて、
M5Stackのボードパッケージのダウンロードでタイムアウトしてしまったので、
そこの解消方法だけここに記載しておきます。
Arduino IDEでM5Stackのボードパッケージのダウンロードでタイムアウトしてしまう問題
https://docs.m5stack.com/ja/arduino/arduino_board
2. Arduino ボード管理の1. インストールボードの管理にて、
M5Stackのボードパッケージのダウンロード中にタイムアウトエラーが起きてしまいました。
Downloading packages
m5stack:esp32-arduino-libs@idf-release_v5.4-858a988d-v1
m5stack:esp-x32@2411
m5stack:xtensa-esp-elf-gdb@14.2_20240403
m5stack:esp-rv32@2411
Failed to install platform: 'm5stack:esp32:3.2.5'.
Error: 4 DEADLINE_EXCEEDED: net/http: request canceled (Client.Timeout or context cancellation while reading body)
Failed to install platform: 'M5Stack:3.2.5'. 4 DEADLINE_EXCEEDED: net/http: request canceled (Client.Timeout or context cancellation while reading body)
この手順自体ダウンロードをArduino IDE側で行わずに、
手動でダウンロードすることで回避できました。
手順中にあるAdditional Board Manager URLsに指定するURLへ遷移すると以下のようなJSONファイルを確認できます。
https://static-cdn.m5stack.com/resource/arduino/package_m5stack_index.json
{
"packages": [
{
"name": "m5stack",
"maintainer": "M5Stack official",
"websiteURL": "https://github.com/m5stack",
"email": " support@m5stack.com",
"help": {
"online": "https://forum.m5stack.com/"
},
"platforms": [
{
"name": "M5Stack",
"architecture": "esp32",
"version": "3.2.5",
"category": "M5Stack",
"url": "https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/m5stack-3.2.5.zip",
"archiveFileName": "esp32-3.2.5.zip",
"checksum": "SHA-256:359821796882142bc4a74e9f16bf2042ab825df47949407d9a600ad5996cca19",
"size": "20460180",
"help": {
"online": ""
},
"boards": [
{
"name": "M5Tab5"
},
{
"name": "M5Core"
},
この中から主にファイルサイズが大きいものをダウンロードします。
そのときのライブラリのバージョンやOSのアーキテクチャによってダウンロードするファイルは変わりますが、参考までに自分がダウンロードしたファイルを以下に示します。
- m5stack-3.2.5
https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/m5stack-3.2.5.zip- ボードマネージャでは
archiveFileNameで指定された名称のファイルを探すため、保存後に名称をesp32-3.2.5.zipへ変更しました。
- idf-release_v5.4
https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.4/esp32-arduino-libs-idf-release_v5.4-858a988d-v1.zip
- esp-x32
https://github.com/espressif/crosstool-NG/releases/download/esp-14.2.0_20241119/xtensa-esp-elf-14.2.0_20241119-x86_64-w64-mingw32.zip
- xtensa-esp-elf-gdb
https://github.com/espressif/binutils-gdb/releases/download/esp-gdb-v14.2_20240403/xtensa-esp-elf-gdb-14.2_20240403-x86_64-w64-mingw32.zip
- esp-rv32
https://github.com/espressif/crosstool-NG/releases/download/esp-14.2.0_20241119/riscv32-esp-elf-14.2.0_20241119-x86_64-w64-mingw32.zip
自分の環境では、ダウンロードしたzipファイルを
C:\Users\ユーザー名\AppData\Local\Arduino15\staging\packagesへ配置したところ、
ボードマネージャからM5Stackのインストールの際に対象のzipのダウンロードはスキップされ、
インストールまで成功するようになりました。
※配置先のパスや挙動は、Arduino IDEのバージョン等により変わる可能性があります。
実装
環境構築ができたので、早速実装を行っていきます。
まずはコイントスから作っていきます。
コイントス
完成品はこちら
#include <M5Unified.h>
// シェイク判定の強さと、クールタイム
const float SHAKE_THRESHOLD = 1.5;
const long SHAKE_COOLDOWN = 500;
// 画面サイズ定数(画面は128x128のため、中央の64)
const int SCREEN_CENTER_X = 64;
const int SCREEN_CENTER_Y = 64;
// 前回の実行時刻
unsigned long lastShakeTime = 0;
void setup() {
auto cfg = M5.config();
M5.begin(cfg);
// 日本語フォントの設定
M5.Display.setFont(&fonts::lgfxJapanGothic_32);
M5.Display.setTextDatum(middle_center);
showWelcomeScreen();
}
void loop() {
M5.update();
// 加速度センサーの値から、ベクトルの大きさを計算
float ax, ay, az;
M5.Imu.getAccel(&ax, &ay, &az);
float force = sqrt(ax*ax + ay*ay + az*az);
runCoinToss(force);
delay(10);
}
/**
* コイントスのメイン処理
* @param force シェイクの強さ
*/
void runCoinToss(float force) {
unsigned long now = millis();
// 連続実行されないように、クールダウン時間を設ける
if (now - lastShakeTime < SHAKE_COOLDOWN) {
return;
}
// しきい値を超えたら実行
if (force > SHAKE_THRESHOLD) {
lastShakeTime = now;
// 結果が出るまでの演出として、「表」と「裏」を交互に表示
for (int i = 0; i < 15; i++) {
M5.Display.fillScreen(i % 2 == 0 ? TFT_RED : TFT_BLUE);
M5.Display.setTextColor(TFT_WHITE);
M5.Display.setTextSize(2);
M5.Display.drawString(i % 2 == 0 ? "表" : "裏", SCREEN_CENTER_X, SCREEN_CENTER_Y);
// だんだん表示がゆっくりになるようにdelayを追加
delay(50 + i * 10);
}
// ランダムで「表」か「裏」を決定
int result = random(2);
M5.Display.fillScreen(result == 0 ? TFT_RED : TFT_BLUE);
M5.Display.setTextColor(TFT_WHITE);
M5.Display.setTextSize(2);
M5.Display.drawString(result == 0 ? "表" : "裏", SCREEN_CENTER_X, SCREEN_CENTER_Y);
}
}
/**
* 初期画面表示
*/
void showWelcomeScreen() {
M5.Display.setRotation(0);
M5.Display.fillScreen(TFT_BLACK);
M5.Display.setTextColor(TFT_WHITE);
M5.Display.setTextSize(1);
M5.Display.drawString("Mode:", SCREEN_CENTER_X, 50);
M5.Display.setTextColor(TFT_GREEN);
M5.Display.drawString("COIN", SCREEN_CENTER_X, 90);
delay(1000);
M5.Display.clear();
}
コメントしている以上の解説はほぼありませんが、
加速度はx, y, zの3軸で取れるので、√x^2 + y^2 + z^2を計算して大きさを求めます。
ベクトルの大きさは重力の関係で静止時でも1ぐらいはあるようなので、
自分の環境ではしきい値を1.5に設定してみました。
ここは実行して調整してみてください。
// シェイク判定の強さのしきい値
const float SHAKE_THRESHOLD = 1.5;
// 加速度センサーの値から、ベクトルの大きさを計算
float ax, ay, az;
M5.Imu.getAccel(&ax, &ay, &az);
float force = sqrt(ax*ax + ay*ay + az*az);
// しきい値を超えたら実行
if (force > SHAKE_THRESHOLD) {
ダイスロール
続いてダイスロール機能です。
追加した機能はこちら
/**
* サイコロのメイン処理
* @param force シェイクの強さ
*/
void runDiceRoll(float force) {
unsigned long now = millis();
// 連続実行されないように、クールタイムを設ける
if (now - lastShakeTime < SHAKE_COOLDOWN) {
return;
}
// しきい値を超えたら実行
if (force > SHAKE_THRESHOLD) {
lastShakeTime = now;
// 結果が出るまでの演出として、1から6の目をランダムに表示
for (int i = 0; i < 15; i++) {
int r = random(1, 7);
M5.Display.fillScreen(TFT_DARKGREEN);
drawDiceFace(r);
delay(50 + i * 10);
}
// 結果確定:1から6の目を決定
int result = random(1, 7);
M5.Display.fillScreen(TFT_DARKGREEN);
drawDiceFace(result);
}
}
/**
* サイコロの目を描画する関数
*/
void drawDiceFace(int number) {
// サイコロのサイズを定義
int boxSize = 92;
int boxX = (128 - boxSize) / 2;
int boxY = (128 - boxSize) / 2;
// サイコロの枠(白色の四角を描画したのち、内側に緑色の四角を描画することで枠のみを描画)
M5.Display.fillRoundRect(boxX, boxY, boxSize, boxSize, 8, TFT_WHITE);
M5.Display.fillRoundRect(boxX + 4, boxY + 4, boxSize - 8, boxSize - 8, 6, TFT_DARKGREEN);
// ドットサイズの定義
int dotRadius = 6;
// ドットの打つ位置は9通りの位置関係で表せるため、サイコロのサイズの1/4の値を取る
// 1 = (41, 41) 2 = (64, 41) 3 = (87, 41)
// 4 = (41, 64) 5 = (64, 64) 6 = (87, 64)
// 7 = (41, 87) 8 = (64, 87) 9 = (87, 87)
int offset = boxSize / 4; // 23
// 基準点の5 = (64, 64)
int centerX = SCREEN_CENTER_X;
int centerY = SCREEN_CENTER_Y;
// 基準点から上下左右にoffset分ずらし、1~9までの座標を定義できるようにする。
int leftX = centerX - offset;
int rightX = centerX + offset;
int topY = centerY - offset;
int bottomY = centerY + offset;
// 目のパターンに応じてドットを描画
switch(number) {
case 1:
// 基準点のみ(5)
M5.Display.fillCircle(centerX, centerY, dotRadius, TFT_WHITE);
break;
case 2:
// 左上と右下(1, 9)
M5.Display.fillCircle(leftX, topY, dotRadius, TFT_WHITE);
M5.Display.fillCircle(rightX, bottomY, dotRadius, TFT_WHITE);
break;
case 3:
// 左上と基準点と右下(1, 5, 9)
M5.Display.fillCircle(leftX, topY, dotRadius, TFT_WHITE);
M5.Display.fillCircle(centerX, centerY, dotRadius, TFT_WHITE);
M5.Display.fillCircle(rightX, bottomY, dotRadius, TFT_WHITE);
break;
case 4:
// 四隅(1, 3, 7, 9)
M5.Display.fillCircle(leftX, topY, dotRadius, TFT_WHITE);
M5.Display.fillCircle(rightX, topY, dotRadius, TFT_WHITE);
M5.Display.fillCircle(leftX, bottomY, dotRadius, TFT_WHITE);
M5.Display.fillCircle(rightX, bottomY, dotRadius, TFT_WHITE);
break;
case 5:
// 四隅 + 中央(1, 3, 5, 7, 9)
M5.Display.fillCircle(leftX, topY, dotRadius, TFT_WHITE);
M5.Display.fillCircle(rightX, topY, dotRadius, TFT_WHITE);
M5.Display.fillCircle(centerX, centerY, dotRadius, TFT_WHITE);
M5.Display.fillCircle(leftX, bottomY, dotRadius, TFT_WHITE);
M5.Display.fillCircle(rightX, bottomY, dotRadius, TFT_WHITE);
break;
case 6:
// 左右に3つずつ(1, 3, 4, 6, 7, 9)
M5.Display.fillCircle(leftX, topY, dotRadius, TFT_WHITE);
M5.Display.fillCircle(leftX, centerY, dotRadius, TFT_WHITE);
M5.Display.fillCircle(leftX, bottomY, dotRadius, TFT_WHITE);
M5.Display.fillCircle(rightX, topY, dotRadius, TFT_WHITE);
M5.Display.fillCircle(rightX, centerY, dotRadius, TFT_WHITE);
M5.Display.fillCircle(rightX, bottomY, dotRadius, TFT_WHITE);
break;
}
}
少しサイコロの目の表示の定義が複雑ですが、
メイン処理の構造はコイントス側と同じです。
その他の変更点
#include <M5Unified.h>
// シェイク判定の強さと、クールタイム
const float SHAKE_THRESHOLD = 1.5;
const long SHAKE_COOLDOWN = 500;
// 画面サイズ定数(画面は128x128のため、中央の64)
const int SCREEN_CENTER_X = 64;
const int SCREEN_CENTER_Y = 64;
+ // モード定義
+ enum Mode {
+ MODE_COIN = 0,
+ MODE_DICE = 1
+ };
+
+ // 現在のモード
+ int currentMode = MODE_COIN;
// 前回の実行時刻
unsigned long lastShakeTime = 0;
void setup() {
auto cfg = M5.config();
M5.begin(cfg);
// 日本語フォントの設定
M5.Display.setFont(&fonts::lgfxJapanGothic_32);
M5.Display.setTextDatum(middle_center);
- showWelcomeScreen();
+ showModeSwitchEffect(currentMode);
}
void loop() {
M5.update();
+ // ボタン押下時にモード切替
+ if (M5.BtnA.wasPressed()) {
+ currentMode++;
+ // 2を超えたら0に戻す
+ if (currentMode > MODE_DICE) {
+ currentMode = MODE_COIN;
+ }
+
+ // モード切替時のエフェクトを表示
+ showModeSwitchEffect(currentMode);
+ }
// 加速度センサーの値から、ベクトルの大きさを計算
float ax, ay, az;
M5.Imu.getAccel(&ax, &ay, &az);
float force = sqrt(ax*ax + ay*ay + az*az);
- runCoinToss(force);
+ switch (currentMode) {
+ case MODE_COIN:
+ runCoinToss(force);
+ break;
+ case MODE_DICE:
+ runDiceRoll(force);
+ break;
+ }
delay(10);
}
/**
* コイントスのメイン処理
* @param force シェイクの強さ
*/
void runCoinToss(float force) {
// 省略
}
/**
* サイコロのメイン処理
* @param force シェイクの強さ
*/
void runDiceRoll(float force) {
// 省略
}
/**
* サイコロの目を描画する関数
*/
void drawDiceFace(int number) {
// 省略
}
/**
- * 初期画面表示
+ * 画面切り替え時の処理
*/
- void showWelcomeScreen() {
+ void showModeSwitchEffect(int mode) {
M5.Display.setRotation(0);
M5.Display.fillScreen(TFT_BLACK);
M5.Display.setTextColor(TFT_WHITE);
M5.Display.setTextSize(1);
+ String name;
+ switch(mode) {
+ case MODE_COIN:
+ name = "COIN";
+ break;
+ case MODE_DICE:
+ name = "DICE";
+ break;
+ }
M5.Display.drawString("MODE:", SCREEN_CENTER_X, 40);
M5.Display.setTextColor(TFT_GREEN);
- M5.Display.drawString("COIN", SCREEN_CENTER_X, 90);
+ M5.Display.drawString(name, SCREEN_CENTER_X, 80);
delay(1000);
M5.Display.clear();
}
モードの概念を導入し、ボタン押下時にモードの切り替えを行うようにしています。
そして、モードによって実行アクションを切り替えます。
プログラム全文
感想
普段はソフトウェアの開発のみを行っているので、
こういったハード系のプログラムを書いて、
実物として動くものが出来てすごく楽しかったです!
M5Stackさんの製品は可愛いものが多くて、他の製品もついついたくさん買ってしまいました。。。
今度はスタックチャンなどにも挑戦したいと思います!
