ルービックキューブロボットのソフトウェアをアップデートした 6. 機械操作(Arduino)

# この記事はなに？
私は現在2x2x2ルービックキューブを解くロボットを開発中です。これはそのロボットのプログラムの解説記事集です。
![soltvvo3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/494375/1574c772-6b93-bfc3-6603-739b5e8c927f.jpeg)
かつて[こちら](https://qiita.com/Nyanyan_Cube/items/60f3699db96dba4a4bf5)の記事に代表される記事集を書きましたが、この時からソフトウェアが大幅にアップデートされたので新しいプログラムについて紹介しようと思います。

該当するコードは[こちら](https://github.com/Nyanyan/soltvvo/tree/master/Soltvvo3.2)で公開しています。

# 関連する記事集
「ルービックキューブを解くロボットを作ろう！」
1. [概要編](https://qiita.com/Nyanyan_Cube/items/a1b6e6bc7e4ac832b3d0)
2. [アルゴリズム編](https://qiita.com/Nyanyan_Cube/items/60f3699db96dba4a4bf5)
3. [ソフトウェア編](https://qiita.com/Nyanyan_Cube/items/94bfa7b56c8ff69d94cd)
4. [ハードウェア編](https://qiita.com/Nyanyan_Cube/items/8bb5555497e745697412)

ルービックキューブロボットのソフトウェアをアップデートした
1. [基本関数](https://qiita.com/Nyanyan_Cube/items/e225e8e31897d81a1401)
2. [事前計算](https://qiita.com/Nyanyan_Cube/items/495a32adf4bfe970e100)
3. [解法探索](https://qiita.com/Nyanyan_Cube/items/7413ceaad019a931112d)
4. [状態認識](https://qiita.com/Nyanyan_Cube/items/8fba84c078e3d6bd09f9)
5. [機械操作(Python)](https://qiita.com/Nyanyan_Cube/items/fc4717e09f0ede48de35)
6. 機械操作(Arduino)(本記事)
7. [主要処理](https://qiita.com/Nyanyan_Cube/items/1fcb21f6ef23ae483c60)

今回は機械操作(Arduino)編として、```soltvvo3_arduino.ino```を紹介します。

# 定数と変数
グローバルに置いた定数と変数です。

```C++
const int magnet_threshold = 50;
const long turn_steps = 400;
const int step_dir[2] = {11, 9};
const int step_pul[2] = {12, 10};
const int sensor[2] = {14, 15};
const int grab_deg[2] = {74, 74};
const int release_deg[2] = {96, 96};
const int offset = 3;

char buf[30];
int idx = 0;
long data[3];
```
定数はポート番号だったりサーボモーターの角度だったりです。変数はスペース区切りで値が与えられるのを分割する時に使います。

# サーボライブラリ周り
サーボモーターを使う関係でサーボライブラリをインクルードして```servo0, servo1```を定義しておきます。

```C++
#include <Servo.h>

Servo servo0;
Servo servo1;
```

# セットアップ
セットアップです。

```C++
void setup() {
  Serial.begin(115200);
  for (int i = 0; i < 2; i++) {
    pinMode(step_dir[i], OUTPUT);
    pinMode(step_pul[i], OUTPUT);
    pinMode(sensor[i], INPUT);
  }
  servo0.attach(7);
  servo1.attach(8);
  servo0.write(release_deg[0] + 5);
  servo1.write(release_deg[1] + 5);
  delay(70);
  servo0.write(release_deg[0]);
  servo1.write(release_deg[1]);
}
```
ピンのインプット、アウトプットの定義と、サーボモーターを動かしてアームを外側に移動させます。

# アームを回転させる
ステッピングモーターを回す関数です。台形駆動を実装しましたが、その関係でタイマーなどを使わず```delay```を使う、少しカッコ悪い実装です。

```C++
void move_motor(long num, long deg, long spd) {
  bool hl = true;
  if (deg < 0) hl = false;
  digitalWrite(step_dir[num], hl);
  long steps = abs(deg) * turn_steps / 360;
  long avg_time = 1000000 * 60 / turn_steps / spd;
  long max_time = 1500;
  long slope = 50;
  bool motor_hl = false;
  long accel = min(steps / 2, max(0, (max_time - avg_time) / slope));
  int num1 = (num + 1) % 2;
  // 加速
  for (int i = 0; i < accel; i++) {
    motor_hl = !motor_hl;
    digitalWrite(step_pul[num], motor_hl);
    delayMicroseconds(max_time - slope * i);
  }
  // 平常運転
  for (int i = 0; i < steps * 2 - accel * 2; i++) {
    motor_hl = !motor_hl;
    digitalWrite(step_pul[num], motor_hl);
    delayMicroseconds(avg_time);
  }
  // 減速
  for (int i = 0; i < accel; i++) {
    motor_hl = !motor_hl;
    digitalWrite(step_pul[num], motor_hl);
    delayMicroseconds(max_time - slope * accel + accel * (i + 1));
  }
}
```

基本的には以下の流れで処理しています。

1. 角度から回すべきステップ数を計算
2. 回転数からパルスの幅を計算
3. 加速、減速に使うステップ数を計算

# アームのキャリブレーション
アームのステッピングモーターを使っている部分は適宜ちゃんとした角度になるようにキャリブレーションします。このとき、アーム側につけた磁石とステッピングモーター側につけたホールセンサ(磁気センサ)を使います。

```C++
void motor_adjust(long num, long spd) {
  int max_step = 150;
  int delay_time = 800;
  bool motor_hl = false;
  digitalWrite(step_dir[num], LOW);
  while (analogRead(sensor[num]) <= magnet_threshold) {
    motor_hl = !motor_hl;
    digitalWrite(step_pul[num], motor_hl);
    delayMicroseconds(delay_time);
  }
  while (analogRead(sensor[num]) > magnet_threshold) {
    motor_hl = !motor_hl;
    digitalWrite(step_pul[num], motor_hl);
    delayMicroseconds(delay_time);
  }
}
```

1つ目の```while```文がトリッキーですが、すでにアームが正しい位置にあると判定される場合でも、少しアームの角度がずれていることがあるので一回アームをずらしています。

2つ目の```while```文で磁石がホールセンサの前に来るまでステッピングモーターを回転させます。

# パズルを離す
アームを外側に移動させ、パズルを離す関数です。

```C++
void release_arm(int num) {
  if (num == 0)servo0.write(release_deg[num] + offset);
  else servo1.write(release_deg[num] + offset);
}
```

あまり綺麗な書き方ではありません。綺麗な書き方をご存知の方はぜひ教えてください。```offset```については速くモーターを動かすためのオーバーシュートを実装したのですが、オーバーシュートしたまま目標値に戻さなくても良いことがわかり```offset```を足したままになっています。

# パズルを掴む
アームを中心に移動させ、パズルを掴む関数です。

```C++
void grab_arm(int num) {
  if (num == 0)servo0.write(grab_deg[num] - offset);
  else servo1.write(grab_deg[num] - offset);
  delay(70);
  if (num == 0)servo0.write(grab_deg[num]);
  else servo1.write(grab_deg[num]);
}
```

ここでは```offset```で作ったオーバーシュートが役に立ちます。これによってより速くパズルを掴むことを期待しています(実際どれくらい効果があるのかはわかりません)

# メインループ
メインループはコマンドを受け取って該当の関数を呼び出します。

```C++
void loop() {
  while (1) {
    if (Serial.available()) {
      buf[idx] = Serial.read();
      if (buf[idx] == '\n') {
        buf[idx] = '\0';
        data[0] = atoi(strtok(buf, " "));
        data[1] = atoi(strtok(NULL, " "));
        data[2] = atoi(strtok(NULL, " "));
        if (data[1] == 1000) grab_arm(data[0]);
        else if (data[1] == 2000) release_arm(data[0]);
        else if (data[1] == 0) motor_adjust(data[0], data[2]);
        else move_motor(data[0], data[1], data[2]);
        idx = 0;
      }
      else {
        idx++;
      }
    }
  }
}
```

コマンドの種類によって呼び出す関数が違います。

# まとめ
今回は実際にアクチュエータを動かすArduinoのプログラムを紹介しました。私の言語に対する知識不足によるところなのか、一部綺麗でない書き方をしていると思います。ぜひコメントで良い書き方を教えてください。

次回はPythonのメイン処理を解説します。