ネットで見つけた激安ロボットが実際動くまでのハナシ

# はじめ

[ヘキサポッド](https://www.amazon.co.jp/dp/B07QFDPJM5)とか言うカッコよくて安い[^1]の発見したのでポチった！

# 届いた

これ、骨組みしかなくね？[^2]

# 結局使った部品

|名称|個数|購入サイト|補足|
|---|---:|---|---|
|ヘキサポッド/ 6本足ロボット スパイダー ロボットフルセット ブラケット アクセサリー ブラック|1|https://www.amazon.co.jp/dp/B07QFDPJM5|骨組み、ネジ＋ナット、ケーブルスパイラルチューブのセット|
|ネジ＋ナット＋平ワッシャー＋スプリングワッシャー(M2)|*|https://www.amazon.co.jp/dp/B01NCRQHRK|上記セットにM2ネジ＋ナットが含まれていなかったため|
|obniz|1|https://www.amazon.co.jp/dp/B07DD6FK8G|心臓部|
|サーボモータードライバー PCA9685|1|https://www.amazon.co.jp/dp/B07SLRG5J1|1つで16個のサーボモーターを制御可能|
|サーボモーター SG90|12|https://www.amazon.co.jp/dp/B00VUJYNWG|本来6本脚のロボットだが、一身上の都合[^3]により4本脚に|
|カメラ Arducam Mini 2MP|1|https://www.switch-science.com/catalog/3780/|カメラが付いてた方がカッコいいと思った|
|モバイルバッテリー＋USBケーブル(A-MicroB)|1|https://store.shopping.yahoo.co.jp/meiseishop/539a1.html|出力2.1Aの割りに軽かったので採用、電流低いとサーボが上手く動かない…|
|ブレッドボード|1|http://akizukidenshi.com/catalog/g/gP-05155/|小さめのものを採用|
|L型ヘッダーピン|1|http://akizukidenshi.com/catalog/g/gC-05336/|obnizのJ1ピンに半田付けしておくと電源の取り回しが楽になる。[新型](https://blog.obniz.io/ja/news/obniz-board-1y-2/)では不要|
|両側ロングヘッダーピン|1|http://akizukidenshi.com/catalog/g/gC-09056/|obnizをブレッドボードに接続する時に使用|
|ジャンパーワイヤー|*||短い物もあると良い|
|スポンジ両面テープ|*||百均ので十分|
|ケーブルタイ|*||百均ので十分|

部品の山を見ると、完成するか不安になる…
![部品](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/d03a34dc-d010-d8c0-9078-00f7742ba63b.jpeg)

## obniz とは
[公式サイト](https://obniz.io/ja/)
端的に言うとJavascriptで電子工作できヤツ
今回使う部品の[ライブラリ](https://obniz.io/ja/sdk/parts)もあるので、驚くほど簡単に作れる~~ハズ~~

## サーボモータードライバー とは
obniz単体でもサーボモーターの制御は可能だが、流石に12個も制御できない[^4]ので採用

# 大体の手順
## 部品の動作確認
パーツライブラリの[サーボドライバー](https://obniz.io/ja/sdk/parts/PCA9685/README.md)や[サーボ](https://obniz.io/ja/sdk/parts/ServoMotor/README.md)を参考に動作確認を行う
この時、サーボを90°に設定してサーボホーンを付けておくと組み立て時に二度手間にならない…
![サーボの動作確認](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/367b40a1-1a6f-2fc6-45c6-24c214445cf7.jpeg)

[カメラ](https://obniz.io/ja/sdk/parts/ArduCAMMini/README.md)も動作確認しておく
![カメラの動作確認](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/f8c70426-8dd2-83be-2956-9004cfb2be39.jpeg)

## 組み立て
購入サイトの完成図を見ながら骨組みにサーボを取り付けていく
![骨組みにサーボを取り付け](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/56ac2354-12af-b96d-2cae-04fb0f36dbb5.jpeg)
ネジ＋ナット、スポンジ両面テープ、ケーブルタイ等を駆使し、他の部品を盛っていく
![配線](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/fa4a3ad4-a794-359f-71e6-716ccc65d49a.jpeg)

## プログラム
動作確認時に書いたコードを元にロボット制御用のプログラムを作成する
歩行に関しては[このページ](https://makezine.jp/blog/2016/12/robot-quadruped-arduino-program.html)を参考にしたけど、難しいことはわからなかったので、角度は現物合わせした

他に気を付ける点は、obnizはI2C[^5]が1つしか使えないため、サーボモータードライバーとカメラで同じI2Cを使うようコードを書く必要がある

<details><summary>コード(ぼちぼち長いので折り畳み)</summary><div>

```html:コード
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
    <script src="https://obniz.io/js/jquery-3.2.1.min.js"></script>
    <script src="https://unpkg.com/obniz@3.1.0/obniz.js" crossorigin="anonymous"></script>
    <style>
      @media screen and (orientation: landscape) {
        .camera-image {
          width: auto;
          height: 100%;
          right: 0px;
        }
      }

      @media screen and (orientation: portrait) {
        .camera-image {
          width: 100%;
          height: auto;
        }
      }

      body {
        user-select: none;
        -moz-user-select: none;
        -ms-user-select: none;
        -khtml-user-select: none;
        -webkit-user-select: none;
        -webkit-touch-callout: none;
      }

      .camera-image {
        position: absolute;
        z-index: 0;
        border: 2px;
        border-style: solid;
        border-color: dimgray;
      }

      .parent {
        height: 100%;
        width: 100%;
        background-color: darkgray;
        position: relative;
      }

      .moveGroup {
        position: absolute;
        bottom: 210px;
        left: 5px;
        z-index: 1;
      }

      .button {
        display: flex;
        align-items: center;
        justify-content: center;
        width: 100px;
        height: 100px;
        border: 1px solid #000000;
        border-radius: 10% 10% 10% 10%;
        font-size: xx-large;
        opacity: 0.50;
        background-color: white;
      }

      .button-pressed {
        background: #EFEFEF;
      }

      .forward {
        position: absolute;
        top: 0px;
        left: 105px;
      }

      .turn-left {
        position: absolute;
        top: 0px;
        left: 0px;
      }

      .turn-right {
        position: absolute;
        top: 0px;
        left: 210px;
      }

      .left {
        position: absolute;
        top: 105px;
        left: 0px;
      }

      .right {
        position: absolute;
        top: 105px;
        left: 210px;
      }

      .backward {
        position: absolute;
        top: 105px;
        left: 105px;
      }
    </style>
  </head>

  <body>
    <div class="parent">
      <div class="moveGroup">
        <div class="button forward" id="Forward">↑</div>
        <div class="button turn-left" id="TurnLeft">↶</div>
        <div class="button turn-right" id="TurnRight">↷</div>
        <div class="button left" id="Left">←</div>
        <div class="button backward" id="Backward">↓</div>
        <div class="button right" id="Right">→</div>
      </div>
      <img class="camera-image" id="CameraImage">
    </div>
    <script>
      /**
       * スリープ
       * param {number} msec ミリ秒
       */
      const sleep = msec => new Promise(resolve => setTimeout(resolve, msec));
      
      var obniz = new Obniz();

      obniz.onconnect = async function () {
        var i2c = obniz.getFreeI2C();
        i2c.start({ mode: "master", sda: 8, scl: 9, clock: 400000, pull: "5v" });

        var camera = obniz.wired("ArduCAMMini", { cs: 2, mosi: 3, miso: 4, sclk: 5, gnd: 6, i2c: i2c, module_version: 1 });
        await camera.startupWait();
           camera.setSize("176x144");
           await obniz.wait(1000);
        
        await setInterval(async function () {
          let data = await camera.takeWait();
          let base64 = camera.arrayToBase64(data);
          $("#CameraImage").attr("src", "data:image/jpeg;base64, " + base64);
        }, 1000);

        var servoDriver = obniz.wired("PCA9685", { i2c: i2c });
        var rightFront1Servo = obniz.wired("ServoMotor", { pwm: servoDriver.getPWM(0) });
        var rightFront2Servo = obniz.wired("ServoMotor", { pwm: servoDriver.getPWM(1) });
        var rightFront3Servo = obniz.wired("ServoMotor", { pwm: servoDriver.getPWM(2) });
        var rightRear1Servo = obniz.wired("ServoMotor", { pwm: servoDriver.getPWM(4) });
        var rightRear2Servo = obniz.wired("ServoMotor", { pwm: servoDriver.getPWM(5) });
        var rightRear3Servo = obniz.wired("ServoMotor", { pwm: servoDriver.getPWM(6) });
        var leftRear1Servo = obniz.wired("ServoMotor", { pwm: servoDriver.getPWM(8) });
        var leftRear2Servo = obniz.wired("ServoMotor", { pwm: servoDriver.getPWM(9) });
        var leftRear3Servo = obniz.wired("ServoMotor", { pwm: servoDriver.getPWM(10) });
        var leftFront1Servo = obniz.wired("ServoMotor", { pwm: servoDriver.getPWM(12) });
        var leftFront2Servo = obniz.wired("ServoMotor", { pwm: servoDriver.getPWM(13) });
        var leftFront3Servo = obniz.wired("ServoMotor", { pwm: servoDriver.getPWM(14) });

        // 各サーボのパルス幅
        rightFront1Servo.range = { min: 0.5, max: 2.4 }
        rightFront2Servo.range = { min: 0.5, max: 2.4 }
        rightFront3Servo.range = { min: 0.5, max: 2.4 }
        rightRear1Servo.range = { min: 0.5, max: 2.4 }
        rightRear2Servo.range = { min: 0.5, max: 2.4 }
        rightRear3Servo.range = { min: 0.5, max: 2.4 }
        leftRear1Servo.range = { min: 0.5, max: 2.4 }
        leftRear2Servo.range = { min: 0.5, max: 2.4 }
        leftRear3Servo.range = { min: 0.5, max: 2.4 }
        leftFront1Servo.range = { min: 0.5, max: 2.4 }
        leftFront2Servo.range = { min: 0.5, max: 2.4 }
        leftFront3Servo.range = { min: 0.5, max: 2.4 }

        // 各サーボのトリム値
        const rightFront1Trim = -6;
        const rightFront2Trim = 0;
        const rightFront3Trim = 0;
        const rightRear1Trim = 0;
        const rightRear2Trim = 0;
        const rightRear3Trim = 0;
        const leftRear1Trim = -5;
        const leftRear2Trim = -2;
        const leftRear3Trim = -2;
        const leftFront1Trim = 12;
        const leftFront2Trim = 7;
        const leftFront3Trim = 3;

        var isBasePosition = false;
        var isForward = false;
        var isTurnRight = false;
        var isTurnLeft = false;
        var isRight = false;
        var isLeft = false;
        var isBackward = false;
        var isStoped = true;

        /**
         * 右前1角度設定
         * param {number} val 角度
         */
        const rightFront1Angle = function (val) {
            rightFront1Servo.angle(rightFront1Trim + val);
        }

        /**
         * 右前2角度設定
         * param {number} val 角度
         */
        const rightFront2Angle = function (val) {
          rightFront2Servo.angle(180 - (rightFront2Trim + val));
        }

        /**
         * 右前3角度設定
         * param {number} val 角度
         */
        const rightFront3Angle = function (val) {
          rightFront3Servo.angle(180 - (rightFront3Trim + val));
        }

        /**
         * 右後1角度設定
         * param {number} val 角度
         */
        const rightRear1Angle = function (val) {
          rightRear1Servo.angle(180 - (rightRear1Trim + val));
        }

        /**
         * 右後2角度設定
         * param {number} val 角度
         */
        const rightRear2Angle = function (val) {
          rightRear2Servo.angle(rightRear2Trim + val);
        }

        /**
         * 右後3角度設定
         * param {number} val 角度
         */
        const rightRear3Angle = function (val) {
          rightRear3Servo.angle(rightRear3Trim + val);
        }

        /**
         * 左後1角度設定
         * param {number} val 角度
         */
        const leftRear1Angle = function (val) {
          leftRear1Servo.angle(leftRear1Trim + val);
        }

        /**
         * 左後2角度設定
         * param {number} val 角度
         */
        const leftRear2Angle = function (val) {
          leftRear2Servo.angle(180 - (leftRear2Trim + val));
        }

        /**
         * 左後3角度設定
         * param {number} val 角度
         */
        const leftRear3Angle = function (val) {
          leftRear3Servo.angle(180 - (leftRear3Trim + val));
        }

        /**
         * 左前1角度設定
         * param {number} val 角度
         */
        const leftFront1Angle = function (val) {
          leftFront1Servo.angle(180 - (leftFront1Trim + val));
        }

        /**
         * 左前2角度設定
         * param {number} val 角度
         */
        const leftFront2Angle = function (val) {
          leftFront2Servo.angle(leftFront2Trim + val);
        }

        /**
         * 左前3角度設定
         * param {number} val 角度
         */
        const leftFront3Angle = function (val) {
          leftFront3Servo.angle(leftFront3Trim + val);
        }

        /**
         * 右前角度設定
         * param {number} val1 角度1
         * param {number} val2 角度2
         * param {number} val3 角度3
         */
        const rightFrontAngle = function (val1, val2, val3) {
          rightFront1Angle(val1);
          rightFront2Angle(val2);
          rightFront3Angle(val3);
        }

        /**
         * 右後角度設定
         * param {number} val1 角度1
         * param {number} val2 角度2
         * param {number} val3 角度3
         */
        const rightRearAngle = function (val1, val2, val3) {
          rightRear1Angle(val1);
          rightRear2Angle(val2);
          rightRear3Angle(val3);
        }

        /**
         * 左前角度設定
         * param {number} val1 角度1
         * param {number} val2 角度2
         * param {number} val3 角度3
         */
        const leftRearAngle = function (val1, val2, val3) {
          leftRear1Angle(val1);
          leftRear2Angle(val2);
          leftRear3Angle(val3);
        }

        /**
         * 左後角度設定
         * param {number} val1 角度1
         * param {number} val2 角度2
         * param {number} val3 角度3
         */
        const leftFrontAngle = function (val1, val2, val3) {
          leftFront1Angle(val1);
          leftFront2Angle(val2);
          leftFront3Angle(val3);
        }

        /**
         * ベースポジション
         */
        const basePosition = async function () {
          rightFrontAngle(40, 60, 80);
          leftFrontAngle(40, 60, 80);
          rightRearAngle(40, 60, 80);
          leftRearAngle(40, 60, 80);
          await sleep(100);

          isBasePosition = true;
        }

        /**
         * 前進
         */
        const forward = async function () {
          while (isForward) {
            isStoped = false;

            leftRearAngle(40, 90, 100);
            await sleep(100);
            leftRearAngle(40, 60, 100);
            await sleep(100);

            leftFrontAngle(50, 90, 65);
            await sleep(100);
            leftFrontAngle(50, 75, 65);
            await sleep(100);

            rightFrontAngle(40, 60, 100);
            leftFrontAngle(40, 60, 80);
            rightRearAngle(50, 75, 65);
            leftRearAngle(40, 60, 80);
            await sleep(100);

            rightRearAngle(40, 90, 100);
            await sleep(100);
            rightRearAngle(40, 60, 100);
            await sleep(100);

            rightFrontAngle(50, 90, 65);
            await sleep(100);
            rightFrontAngle(50, 75, 65);
            await sleep(100);

            rightFrontAngle(40, 60, 80);
            leftFrontAngle(40, 60, 100);
            rightRearAngle(40, 60, 80);
            leftRearAngle(50, 75, 65);
            await sleep(100);

            isBasePosition = false;
          }

          isStoped = true;
        }

        /**
         * 右旋回
         */
        const turnRight = async function () {
          while (isTurnRight) {
            isStoped = false;

            rightFrontAngle(40, 90, 110);
            await sleep(100);
            rightFrontAngle(40, 60, 110);
            await sleep(100);

            leftRearAngle(40, 90, 110);
            await sleep(100);
            leftRearAngle(40, 60, 110);
            await sleep(100);

            rightRearAngle(40, 90, 50);
            await sleep(100);
            rightRearAngle(40, 60, 50);
            await sleep(100);

            leftFrontAngle(40, 90, 50);
            await sleep(100);
            leftFrontAngle(40, 60, 50);
            await sleep(100);

            basePosition();
          }

          isStoped = true;
        }

        /**
         * 左旋回
         */
        const turnLeft = async function () {
          while (isTurnLeft) {
            isStoped = false;

            leftFrontAngle(40, 90, 110);
            await sleep(100);
            leftFrontAngle(40, 60, 110);
            await sleep(100);

            rightRearAngle(40, 90, 110);
            await sleep(100);
            rightRearAngle(40, 60, 110);
            await sleep(100);

            leftRearAngle(40, 90, 50);
            await sleep(100);
            leftRearAngle(40, 60, 50);
            await sleep(100);

            rightFrontAngle(40, 90, 50);
            await sleep(100);
            rightFrontAngle(40, 60, 50);
            await sleep(100);

            basePosition();
          }

          isStoped = true;
        }

        /**
         * 右移動
         */
        const right = async function () {
          while (isRight) {
            isStoped = false;

            rightFrontAngle(60, 90, 80);
            await sleep(100);
            rightFrontAngle(60, 80, 80);
            await sleep(100);

            rightRearAngle(40, 90, 80);
            await sleep(100);
            rightRearAngle(60, 80, 80);
            await sleep(100);

            leftFrontAngle(20, 90, 80);
            await sleep(100);
            leftFrontAngle(20, 40, 80);
            await sleep(100);

            leftRearAngle(20, 90, 80);
            await sleep(100);
            leftRearAngle(20, 40, 80);
            await sleep(100);

            basePosition();
          }

          isStoped = true;
        }

        /**
         * 左移動
         */
        const left = async function () {
          while (isLeft) {
            isStoped = false;

            leftFrontAngle(60, 90, 80);
            await sleep(100);
            leftFrontAngle(60, 80, 80);
            await sleep(100);

            leftRearAngle(40, 90, 80);
            await sleep(100);
            leftRearAngle(60, 80, 80);
            await sleep(100);

            rightFrontAngle(20, 90, 80);
            await sleep(100);
            rightFrontAngle(20, 40, 80);
            await sleep(100);

            rightRearAngle(20, 90, 80);
            await sleep(100);
            rightRearAngle(20, 40, 80);
            await sleep(100);

            basePosition();
          }

          isStoped = true;
        }

        /**
         * 後退
         */
        const backward = async function () {
          while (isBackward) {
            isStoped = false;

            if (isBasePosition) {
              leftFrontAngle(40, 90, 100);
              await sleep(100);
              leftFrontAngle(40, 60, 100);
              await sleep(100);

              leftRearAngle(50, 90, 65);
              await sleep(100);
              leftRearAngle(50, 75, 65);
              await sleep(100);
            }

            rightFrontAngle(50, 75, 65);
            leftFrontAngle(40, 60, 80);
            rightRearAngle(40, 60, 100);
            leftRearAngle(40, 60, 80);
            await sleep(100);

            rightFrontAngle(40, 90, 100);
            await sleep(100);
            rightFrontAngle(40, 60, 100);
            await sleep(100);

            rightRearAngle(50, 90, 65);
            await sleep(100);
            rightRearAngle(50, 75, 65);
            await sleep(100);

            rightFrontAngle(40, 60, 80);
            leftFrontAngle(50, 75, 65);
            rightRearAngle(40, 60, 80);
            leftRearAngle(40, 60, 100);
            await sleep(100);

            leftFrontAngle(40, 90, 100);
            await sleep(100);
            leftFrontAngle(40, 60, 100);
            await sleep(100);

            leftRearAngle(50, 90, 65);
            await sleep(100);
            leftRearAngle(50, 75, 65);
            await sleep(100);

            isBasePosition = false;
          }

          isStoped = true;
        }

        basePosition();

        /**
         * 前進ボタン
         */
        $("#Forward").on("mousedown touchstart", async function () {
          $(this).addClass("button-pressed");
          if (isStoped) {
            isForward = true;
            await forward();
          }
        }).on("mouseup mouseleave touchend", async function () {
          $(this).removeClass("button-pressed");
          isForward = false;
        });

        /**
         * 右旋回ボタン
         */
        $("#TurnRight").on("mousedown touchstart", async function () {
          $(this).addClass("button-pressed");
          if (isStoped) {
            isTurnRight = true;
            turnRight();
          }
        }).on("mouseup mouseleave touchend", async function () {
          $(this).removeClass("button-pressed");
          isTurnRight = false;
        });

        /**
         * 左旋回ボタン
         */
        $("#TurnLeft").on("mousedown touchstart", async function () {
          $(this).addClass("button-pressed");
          if (isStoped) {
            isTurnLeft = true;
            turnLeft();
          }
        }).on("mouseup mouseleave touchend", async function () {
          $(this).removeClass("button-pressed");
          isTurnLeft = false;
        });

        /**
         * 右移動ボタン
         */
        $("#Right").on("mousedown touchstart", async function () {
          $(this).addClass("button-pressed");
          if (isStoped) {
            isRight = true;
            right();
          }
        }).on("mouseup mouseleave touchend", async function () {
          $(this).removeClass("button-pressed");
          isRight = false;
        });

        /**
         * 左移動ボタン
         */
        $("#Left").on("mousedown touchstart", async function () {
          $(this).addClass("button-pressed");
          if (isStoped) {
            isLeft = true;
            left();
          }
        }).on("mouseup mouseleave touchend", async function () {
          $(this).removeClass("button-pressed");
          isLeft = false;
        });

        /**
         * 後退ボタン
         */
        $("#Backward").on("mousedown touchstart", async function () {
          $(this).addClass("button-pressed");
          if (isStoped) {
            isBackward = true;
            backward();
          }
        }).on("mouseup mouseleave touchend", async function () {
          $(this).removeClass("button-pressed");
          isBackward = false;
        });
      }
    </script>
  </body>
</html>
```
</div></details>

# 完成
結構カッコよくね？
![完成品](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/f41402e6-6914-7ae8-46bc-920e4b710da5.jpeg)

スマホでの操作画面、コントローラーとカメラ映像ももちろん表示！
![スマホでの操作画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/4a780bde-ce32-96ec-f41c-79a0db725ae8.png)

動画
https://twitter.com/yamazin/status/1202192900198518786

# 感想
obnizは簡単に電子工作できるのでオススメ！
サーボモーター1つの価格は大したことないけど12個も使うと結構高い…

[^1]: 当時1,923円
[^2]: 説明の紙すら入ってない
[^3]: サーボモーターは思ったより高い！
[^4]: [6個まで行ける](https://forum.obniz.io/topic/78/%E3%82%B5%E3%83%BC%E3%83%9C%E3%83%A2%E3%83%BC%E3%82%BF%E3%83%BC%E6%9C%80%E5%A4%A7%E6%8E%A5%E7%B6%9A%E5%8F%B0%E6%95%B0)らしい
[^5]: シリアル通信の一種、2本の信号線で1つのマスタから複数のスレーブと通信できる