🧲

【電子工作】ルービックキューブを解くマシンを製作した話

に公開

はじめに

2024年の大学祭で、ルービックキューブを物理的に解くマシンを製作した。製作期間は約1ヶ月、ソフト/ハード共に製作した。もともと私はスピードキューブ(ルービックキューブの早解き)を趣味で行っており、その延長で「物理的に自動で解くマシンを作ってみたい」と思い立ったのがきっかけだった。あれから随分期間が空いてしまったが、自分自身の記録として書き残しておこうと思い、今更ながら記事を書くことにした。

何を作ったのか

まずはこちらの動画をご覧いただきたい。
https://youtu.be/we3MKKmyThs

本プロジェクトでは、大きく分けて2つのものを製作した:

  • ルービックキューブの面を物理的に回転させる装置
  • キューブの配色を認識して解法ライブラリに渡すiOSアプリ

解法アルゴリズムには、Pythonライブラリ"kociemba"を使用した。solve()関数を使って、ルービックキューブの状態を54文字の文字列(6面×9マス)として与えると、回転記号(※1)の列を返してくれる。

※1 ルービックキューブの回転方向を表す記号。6面のセンターキューブの位置は変わらないため、回転方向をこのように定義できる。

3x3x3 回転記号 | TORIBOより

ということで今回メインで私が製作したものは主に2つあり、1つは物理的にキューブの面を回転させられる装置、もう1つはキューブの配色の色を認識してライブラリが扱える文字列の形式に変換するアプリ(iOS)である。

材料(主要なもののみ抜粋)

総費用はおよそ15,000円。設計ミスややり直しを含めた実質的な支出は30,000円ほど

使用機材(開発環境)

  • MacBook Air M1
  • 安定化電源 DP100(←めちゃくちゃおすすめ)
  • Raise 3D E2
  • Blender
  • Arduino IDE
  • Xcode15.4(Xcode16.3でも動作確認済み)
  • iPhone15/iPhone16Pro(両動作確認済み)

全体構成と動作の流れ

本マシンの処理全体の流れは以下の通り:

  1. スマホアプリでキューブ6面の色を認識し、文字列を生成
  2. Firebase RealtimeDB にアップロード
  3. DBの変化をPCが検知し、kociembaライブラリで解法を計算して再度DBに書き込み
  4. アプリが解法を読み取り、ESP32にHTTPで送信
  5. ESP32がArduino Megaにシリアル通信で命令を送信
  6. Arduinoが受け取った文字列に従ってモーターを制御

PCに直接接続すればよりシンプルに構成できるが、無線で制御したいというロマンを優先してこの設計にした。盤面文字列もローカルネットワークで共有すればよかったのだが、私の技術不足でうまくいかなかったのでfirebaseを使用した。

製作1. ステッピングモーター制御

6面それぞれの面を90°単位で正確に制御する必要があり、苦戦した部分である。ST-42BYG020モーターとNJW4350Dドライバーを組み合わせ、以下のような配線で制御可能となった。
データシートと睨めっこしたり、AIや有識者に訊いたりしてなんとか以下のような結線で自在に動かすことができた。

この配線図を見て分かる通り、1つのモーターを制御するにあたり、dirピンとstepピンの2つが必要になる。6面それぞれの制御をするためには、2×6=12個のGPIOが必要となる。そのためGPIOの多いArduino megaを使用した。

以下はモーター制御の基本コード:

const int delayTime = 1000;  // パルス間隔
const int stepsPerRevolution = 200;  // モーターの1回転あたりのステップ数
const int dirPin = 30;
const int stepPin = 31;

このようにピン番号とステッピングモーター1回転あたりのステップ数(データシートに記載されていた)、パルスを与える間隔(適宜調整、短すぎると正確に動かなかった)を定義し、

digitalWrite(dirPin, HIGH);
      for(int i = 0 ; i < stepsPerRevolution/4 < i++) {
        digitalWrite(stepPin, HIGH);
        delayMicroseconds(delayTime);
        digitalWrite(stepPin, LOW);
        delayMicroseconds(delayTime);
      }

このような実装で正確に90°ずつ回転させることができた。なお、stepPinはHIGHでは正方向(反時計回り)に回転し、LOWでは時計回りに回転する。

なお、モーターの動作電圧は12Vとデータシートには記載されていたが、あまりにも発熱したため、実用では9Vを印加して動作させていた。トルクもルービックキューブの回転には得に問題なかった。

モーターには保持トルクがあり、電圧を印加した時点で角度が固定されその位置からの相対角で制御される。

製作2. パーツを組み合わせる

実際の完成画像を以下に示す。木材とブレッドボードをうまく使い、可能な限り省スペースでマイコン×2,モータードライバIC×6,モーター×6,キューブを組み合わせている。

大体この写真が全てなわけだが、特に工夫した部分について触れていく。配線が雑すぎるのは許してほしい。

ダイソーが大学から近かったので、百均の木材をめちゃくちゃ活用した。一番下の版と横にICが6個ついてる板は合わせて200円(ノコギリでギコギコした)だった気もするが割と頑丈でキャンパス間の移動の際に非常に役立った。

完成品がA4コピー用紙の箱にジャストサイズだったので、その箱に入れていい感じに持ち運べた。

モーターが付いている赤いパーツは、ちょうどいいものがなかったのと、せっかく部室に3Dプリンタがあったので活用しようと思いジャストサイズで設計して印刷した。
なおパーツ同士の接着は全て3Mの両面テープを使用している。

センターキューブはモーターシャフトに固定し、そのまま周囲のパーツをはめ込む構造にした。若干の遊びがあったほうが動作がスムーズだった。

そのためキューブはもちろん人の手で混ぜられる(もちろんマシンでも混ぜることができるが)が、センターパーツはモーターに付けっぱなしなので混ぜるときにセンターパーツがない状態…そこはご愛嬌。

U面(キューブの一番上の面)にモーターをセットする部分の構造についてはかなり苦戦したが、最終的に画像のような形になった。学祭で3日間にわたり数十回程度実演を行ったが、奇跡的に全て成功した。ちなみに半年後の新歓祭では両面テープが弱っており事故ってしまった

製作3. マシンが動作するまでの流れ

何やらマイコンが2つもついていて、結構回りくどいことをしているので、色認識からマシンが動作するまでの流れを示す。

1.スマホアプリで色認識をして、文字列を生成
2.FirebaseRealtimeDBに送信
3.dbの変更を感知してPCが解法を計算し、再びdbを更新
4.スマホ側で解を読みとる
5.あらかじめマイコンでたてておいたHTTPサーバーにPOST
6.受信したマイコン(ESP32)がシリアル通信でモーターを制御している別のマイコン(Arduino mega)に解法文字列を送信
7.文字列をモーターの回転に変換して実際に駆動

このような順で動作している。PCと繋いでおけばわざわざマイコンを噛ませて無線通信する必要はないのだが、なんとなくこのマシンは無線で全て制御したかったというロマンがあったため、このようにしてしまった。

製作4. 色認識スマホアプリ

もともと私は趣味でiOSアプリの開発を行っていたこともあり、スマホのカメラを利用した色認識が真っ先に思いついた。
色の認識にはOpenCVを利用した。
具体的なSwiftとC++のOpenCVライブラリのブリッジ手順については他サイト等で紹介されているため割愛するが、認識におけるロジックに関しては少々工夫したところがあるため紹介させていただく。

色の定義

まず、ルービックキューブの色は6色で構成されており、それをiPhoneのカメラで読み取ったときのカラーコードを把握する必要がある。
デバックコンソールにカラーコードを出力させながら、使用するキューブに合わせて色の定義をしていった。最終的に以下のような色定義となった。

std::vector<std::pair<cv::Scalar, char>> predefinedColors = {
        {cv::Scalar(25, 40, 255), 'R'},  // 赤 Right
        {cv::Scalar(50, 235, 0), 'F'},  // 緑 Front
        {cv::Scalar(240, 145, 0), 'B'},  // 青 Back
        {cv::Scalar(0, 240, 170), 'D'}, // 黄 Down
        {cv::Scalar(30, 72, 255), 'L'}, // 橙 Left
        {cv::Scalar(230, 230, 230), 'U'}  // 白 Up
};

色の判定

まず、撮影した写真は以下の写真をみて分かるように正方形であり9つのブロックに分かれている。

その一つひとつ(センターを除く)について、以下のように円形でトリミングを行い、その中の平均色を算出する。

int cellX = j * cellWidth + cellWidth / 4;
int cellY = i * cellHeight + cellHeight / 4;
int centerX = cellX + cellWidth / 4; // セルの中心X座標
int centerY = cellY + cellHeight / 4; // セルの中心Y座標

// 半径50ピクセルの円の範囲を定義
cv::Rect boundingBox(centerX - 50, centerY - 50, 100, 100);

// バウンディングボックスの範囲が画像の範囲内に収まるようにクリッピング
boundingBox &= cv::Rect(0, 0, mat.cols, mat.rows);

// バウンディングボックス内の平均色を取得
cv::Mat cellMat = mat(boundingBox);
cv::Scalar avgColor = cv::mean(cellMat);
char colorChar = [self classifyColor:avgColor];
detectedColors += colorChar;

こうすることで、撮影時に多少ズレたりしても正確に色を読み取ることができる。

ここでの処理において、1面分の配色情報文字列detectedColorsに1色に割り当てられている文字colorCharを追加しているのだが、classifyColorで色を分類していることがわかると思う。
このclassifyColorの中身(一部)は以下のようになっている。

for (const auto& predefinedColor : predefinedColors) {
        cv::Scalar predefinedBGR = predefinedColor.first;
        char colorChar = predefinedColor.second;

        // ユークリッド距離を計算
        double distance = sqrt(pow(predefinedBGR[0] - b, 2) +
                               pow(predefinedBGR[1] - g, 2) +
                               pow(predefinedBGR[2] - r, 2));

        // 最も近い色を更新
        if (distance < minDistance) {
            minDistance = distance;
            closestColor = colorChar;
        }

先ほど定義した各色のカラーコードからユークリッド色空間の距離が最も近いものをその色として判断するようにしている。

このようなメソッドで色を6面分解析することができた。

アプリ全体の構造

カメラ起動→6面撮影→配色確認(→再撮影可能)→サーバーで処理→マイコンにPOSTというフローで構築。UIはSwiftUIで実装し、展開図表示機能で色の誤認識等を視覚的にわかるようにした。実演時にも、来場者に直感的に説明できたのは大きい。

実装で苦労した点

OpenCVをSwiftとブリッジするのに苦戦。画像データのMat形式への変換や、処理精度の調整にも時間を要した。UI面ではSwiftUIの恩恵で効率的に構築できた。

おわりに

「ルービックキューブを物理的に解く装置を作りたい!」という思いつきから始まり、ソフト・ハード・構造設計など多くの学びがあった。
また、来場者からたくさんの拍手や称賛の言葉をいただき非常に嬉しかった。これからも面白いものを製作していきたい。

また、本プロジェクトで使用したコードは全て以下で公開している。様々な面でまだまだ改善の余地はあると思う。いつか精度を高めてリメイクしてみたい。
https://github.com/myml12/CubeSolver

Discussion