obnizつかってJavaScriptでラジコンカー作った

# 前提
2018年夏ごろ「obniz」とかいうオモチャを発見したので、買って遊んだ時のハナシ

# 「obniz」とは

![obniz](https://qiita-image-store.s3.amazonaws.com/0/348583/22fe6ce7-f8f5-0b07-8718-2a3bafc0d098.jpeg)
https://obniz.io/
お手軽にIoT製品とか作れる！すごいオモチャ！！
専用クラウドサービス（obnizクラウド）も使い放題！[^1]
Wi-fi経由でobnizクラウドに接続し、JavaScriptで制御できるすごいヤツ！
読み方は「オブナイズ」らしいよ！

# どうやって遊ぶか
出来るだけ簡単で、作った後に遊べそうなモノにしたかったので
**ラジコンカー！[^2]**~~（よくあるやつ）~~

# obniz以外で必要なモノ
### モバイルバッテリー＋USBケーブル
![モバイルバッテリー＋USBケーブル](https://qiita-image-store.s3.amazonaws.com/0/348583/0d8aa3bd-fcc9-4ee0-89e7-47758651e1b0.jpeg)
obnizの電源にはモバイルバッテリーを使用できます。
~~緊急時にコンビニで買った~~乾電池を使用するタイプを使用しました。

### DCモーター＋タイヤ
![DCモーター＋タイヤ](https://qiita-image-store.s3.amazonaws.com/0/348583/e33bc6e3-a34a-dc1b-6b02-7d0880ee6bb4.jpeg)
DCモーターはミニ四駆とかに入ってる直流電源で動くモーターで、
今回はDCモーターとギアボックスとタイヤが合体してる奴を使いました。

### キャスター
![キャスター](https://qiita-image-store.s3.amazonaws.com/0/348583/4365d3c9-506e-df9f-5b2d-1dc34a17a44f.jpeg)
~~DCモーター4個も買いたくなかったので、~~FF駆動（ロントエンジン・フロントドライブ）とし、
後輪はキャスター（100均）を使用しました。

### ジャンパーワイヤー
![ジャンパーワイヤー](https://qiita-image-store.s3.amazonaws.com/0/348583/8483ea3f-e1d7-7079-af77-d3703803b559.jpeg)
オスメスの端子がついてる電線で、DCモーターをobnizに繋ぐのに使用しました。
（DCモーターの端子に、オス端子のある線を半田付け）

### シャーシ
カラーボード（100均）を使用

### その他
~~飽きたら解体するので~~各パーツの接着は、両面テープを使用しました。

# ハードウェア完成品
![本体](https://qiita-image-store.s3.amazonaws.com/0/348583/a7148703-d89e-41bc-5d58-358b8144bfc2.jpeg)
割とカッコいい？

# ソフトウェアで使ったライブラリー
### obniz javascript SDK
これだけでobniz制御できる！すごい！！
https://obniz.io/doc/obnizjs_doc

### JQuery
みんな大好き！~~Vue.js！~~
https://jquery.com/

### JCanvas
HTML5 CanvasをJQueryで操作する奴（初めて使った！）
https://projects.calebevans.me/jcanvas/

# obnizによるDCモーター制御部分
取り合えずobnizにモバイルバッテリーとDCモーターを接続
![obnizに接続](https://qiita-image-store.s3.amazonaws.com/0/348583/280e1d43-1cd1-61a7-d6de-743525d5bcfc.jpeg)

マニュアルみて、書いてみたらめっちゃ簡単に動いた！
https://obniz.io/sdk/parts/DCMotor/README.md?iframe=false

```javascript:宣言回り
/** obniz sdk */
let obniz = new Obniz();
/** 左モーター */
let leftMotor = null;
/** 右モーター */
let rightMotor = null;

/**
 * obniz接続
 */
obniz.onconnect = async () => {
  // ハードウェア初期設定
  leftMotor = obniz.wired("DCMotor", {forward: 0, back: 1}); // 左モーター
  rightMotor = obniz.wired("DCMotor", {forward: 2, back: 3}); // 右モーター
```

```javascript:DCモーター制御用の関数
/**
 * モーター制御
 * param {number} leftPower 左モーターパワー
 * param {number} leftMove 左モーター回転方向
 * param {number} rightPower 右モーターパワー
 * param {number} rightMove 右モーター回転方向
 */
let motorControl = (leftPower, leftMove, rightPower, rightMove) =>  {
  // モーターが設定されていなければ中断
  if (!leftMotor || !rightMotor) return;
        
  leftMotor.power(leftPower);
  rightMotor.power(rightPower);

  switch(leftMove) {
    case MOTER_MOVE_FORWARD:
      leftMotor.forward();
      break;
    case MOTER_MOVE_REVERSE:
      leftMotor.reverse();
      break;
    default:
      leftMotor.stop();
  }

  switch(rightMove) {
    case MOTER_MOVE_FORWARD:
      rightMotor.forward();
      break;
    case MOTER_MOVE_REVERSE:
      rightMotor.reverse();
      break;
    default:
      rightMotor.stop();
  }
}
```
# 画面操作系
あまりに簡単に動いたんで、~~暇つぶしに~~画面操作部分を少し凝ってみました。
機能としては

- PCのマウスでも、スマホのタッチでも操作可能
- スマホの縦向き、横向きどちらでも操作可能
- タッチしている間だけ表示される、バーチャルパッドのスティック風

ちなみにJCanvasを使ってCanvasへの作画はこんな感じでです。

```javascript:バーチャルパッドスティック作画の関数
/**
 * バーチャルパッドスティック作画
 * param {number} x X座標
 * param {number} y Y座標
 */
let drawVirtualPadStick = (x, y) => {
  $("#VirtualPad")
    .removeLayerGroup("VirtualPadStick")
    .drawLayers()
    .drawArc({layer: true,
              name: 'StickCircle',
              groups: ['VirtualPadStick'],
              strokeStyle: 'rgba(0, 0, 0, 0.5)',
              fillStyle: 'rgba(0, 0, 0, 0.5)',
              strokeWidth: 1,
              radius: STICK_RADIUS,
              x: x - $("#VirtualPad").offset().left - STICK_RADIUS,
              y: y - $("#VirtualPad").offset().top - STICK_RADIUS
     }); // スティックの円
}
```

<details><summary>バーチャルパットのサンプル（HTMLとして保存したら動くハズ）</summary><div>

```html:バーチャルパッドのサンプル
<!DOCTYPE html>
<html lang="jp">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <style>
      body {
        margin: 0;
        overflow: hidden;
      }
      canvas {
        position: absolute;
      }
    </style>
    <title>Virtual Pad Sample</title>
  </head>
  <body>
    <canvas id="Screen"></canvas>
    <canvas id="VirtualPad"></canvas>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jcanvas/21.0.1/min/jcanvas.min.js" integrity="sha256-rUshvLY805GcMilbPNnko2JxFKj254/GJZwIP6yaiEk=" crossorigin="anonymous"></script>
    <script>
      // jCanvasの座標の基準を左上に設定
      $.jCanvas.defaults.fromCenter = false;

      /** スティックの閾値 */
      const STICK_THRESHOLD = 150;
      /** スティックの半径 */
      const STICK_RADIUS = 20;

      /** 初期X座標 */
      let defaultX = -1;
      /** 初期Y座標 */
      let defaultY = -1;
      
      /**
       * タッチデバイス判定
       * return {boolean} 判定結果
       */
      let isTouchDevice = () => window.ontouchstart === null;

      /**
       * 閾値に丸める
       * param {number} value 丸める前の値
       * param {number} threshold 閾値
       * return {number} 丸めた後の値
       */
      let roundToThreshold = (value, threshold) => {
        let result;

        if (value > threshold) {
          result = threshold;
        } else if (value < threshold * -1) {
          result = threshold * -1;
        } else {
          result = value
        }

        return result;
      }

      /**
       * バーチャルパッドベース作画
       * param {number} x X座標
       * param {number} y Y座標
       */
      let drawVirtualPadBase = (x, y) => {
        $("#VirtualPad")
          .drawArc({layer: true,
                    name: 'CenterCircle',
                    groups: ['VirtualPadBase'],
                    strokeStyle: 'rgba(0, 0, 0, 0.5)',
                    strokeWidth: 1,
                    radius: STICK_RADIUS,
                    x: x - $("#VirtualPad").offset().left - STICK_RADIUS,
                    y: y - $("#VirtualPad").offset().top - STICK_RADIUS
           }) // 中央の円
          .drawRect({layer: true,
                     name: 'FrameRect',
                     groups: ['VirtualPadBase'],
                     strokeStyle: 'rgba(0, 0, 0, 0.5)',
                     fillStyle: 'rgba(0, 0, 0, 0.05)',          
                     strokeWidth: 1,
                     cornerRadius: STICK_RADIUS,
                     x: x - $("#VirtualPad").offset().left - STICK_THRESHOLD - STICK_RADIUS,
                     y: y - $("#VirtualPad").offset().top - STICK_THRESHOLD - STICK_RADIUS,
                     width: STICK_THRESHOLD * 2 + STICK_RADIUS * 2,
                     height: STICK_THRESHOLD * 2 + STICK_RADIUS * 2
           }) // 枠の四角
          .drawLine({layer: true,
                     name: 'TopLine',
                     groups: ['VirtualPadBase'],
                     strokeStyle: 'rgba(0, 0, 0, 0.5)',
                     strokeWidth: 1,
                     rounded: true,
                     x1: x - $("#VirtualPad").offset().left,
                     y1: y - $("#VirtualPad").offset().top - STICK_THRESHOLD,
                     x2: x - $("#VirtualPad").offset().left,
                     y2: y - $("#VirtualPad").offset().top - STICK_THRESHOLD - STICK_RADIUS
           }) // 上線
          .drawLine({layer: true,
                     name: 'BottomLine',
                     groups: ['VirtualPadBase'],
                     strokeStyle: 'rgba(0, 0, 0, 0.5)',
                     strokeWidth: 1,
                     rounded: true,
                     x1: x - $("#VirtualPad").offset().left,
                     y1: y - $("#VirtualPad").offset().top + STICK_THRESHOLD,
                     x2: x - $("#VirtualPad").offset().left,
                     y2: y - $("#VirtualPad").offset().top + STICK_THRESHOLD + STICK_RADIUS,
           }) // 下線
          .drawLine({layer: true,
                     name: 'LeftLine',
                     groups: ['VirtualPadBase'],
                     strokeStyle: 'rgba(0, 0, 0, 0.5)',
                     strokeWidth: 1,
                     rounded: true,
                     x1: x - $("#VirtualPad").offset().left - STICK_THRESHOLD,
                     y1: y - $("#VirtualPad").offset().top,
                     x2: x - $("#VirtualPad").offset().left - STICK_THRESHOLD - STICK_RADIUS,
                     y2: y - $("#VirtualPad").offset().top
           })  // 左線
          .drawLine({layer: true,
                     name: 'RightLine',
                     groups: ['VirtualPadBase'],
                     strokeStyle: 'rgba(0, 0, 0, 0.5)',
                     strokeWidth: 1,
                     rounded: true,
                     x1: x - $("#VirtualPad").offset().left + STICK_THRESHOLD,
                     y1: y - $("#VirtualPad").offset().top,
                     x2: x - $("#VirtualPad").offset().left + STICK_THRESHOLD + STICK_RADIUS,
                     y2: y - $("#VirtualPad").offset().top
           }); // 右線

        // バーチャルパッドスティック作画
        drawVirtualPadStick(x, y);
      }

      /**
       * バーチャルパッドスティック作画
       * param {number} x X座標
       * param {number} y Y座標
       */
      let drawVirtualPadStick = (x, y) => {
        $("#VirtualPad")
          .removeLayerGroup("VirtualPadStick")
          .drawLayers()
          .drawArc({layer: true,
                    name: 'StickCircle',
                    groups: ['VirtualPadStick'],
                    strokeStyle: 'rgba(0, 0, 0, 0.5)',
                    fillStyle: 'rgba(0, 0, 0, 0.5)',
                    strokeWidth: 1,
                    radius: STICK_RADIUS,
                    x: x - $("#VirtualPad").offset().left - STICK_RADIUS,
                    y: y - $("#VirtualPad").offset().top - STICK_RADIUS
           }); // スティックの円
      }

      /**
       * バーチャルパットクリア
       */
      let clearVirtualPad = () => {
        $("#VirtualPad")
          .removeLayerGroup("VirtualPadBase")
          .removeLayerGroup("VirtualPadStick")
          .drawLayers();
      }

      /**
       * ロード、画面回転、サイズ変更
       */
      $(window).on("load orientationchange resize", () => {
        // バーチャルパッド用キャンバスを画面サイズに合わせる
        $("#VirtualPad").get(0).width = $(window).width();
        $("#VirtualPad").get(0).height = $(window).height();

        // 画面用キャンバスを画面サイズに合わせる
        $("#Screen").get(0).width = $(window).width();
        $("#Screen").get(0).height = $(window).height();

        // バーチャルパッドクリア
        clearVirtualPad();
      });

      /**
       * バーチャルパッド用キャンバス上でタッチ開始、マウスダウン
       */
      $("#VirtualPad").on("touchstart mousedown", () => {
        event.preventDefault();

        // 初期座標取得
        defaultX = isTouchDevice() ? event.changedTouches[0].pageX : event.pageX;
        defaultY = isTouchDevice() ? event.changedTouches[0].pageY : event.pageY;

        // バーチャルパッドベース作画
        drawVirtualPadBase(defaultX, defaultY);

        /**
         * バーチャルパッド用キャンバス上でタッチ移動、マウス移動
         */
        $("#VirtualPad").on("touchmove mousemove", () => {
          event.preventDefault();

          // 移動後の座標取得
          let tempX = roundToThreshold(defaultX - (isTouchDevice() ? event.changedTouches[0].pageX : event.pageX), STICK_THRESHOLD);
          let tempY = roundToThreshold(defaultY- (isTouchDevice() ? event.changedTouches[0].pageY : event.pageY), STICK_THRESHOLD);

          // バーチャルパッドスティック作画
          drawVirtualPadStick(defaultX - tempX, defaultY - tempY);
        });

        /**
         * バーチャルパッド用キャンバス上でタッチ終了、マウスアップ、マウスが離れた
         */
        $("#VirtualPad").on("touchend mouseup mouseleave", () => {
          event.preventDefault();

          $("#VirtualPad").off("touchmove mousemove");

          // バーチャルパッドクリア
          clearVirtualPad();
        });
      });
    </script>
  </body>
</html>
```

</div></details>

んで、obnizによるDCモーター制御部分のソースを組み込んで、ちょいちょい弄って終了。
# ソフトウェア完成
![操作画面](https://qiita-image-store.s3.amazonaws.com/0/348583/1ede3b36-e40b-7614-16d8-f04366b83176.png)

動かしたらこんな感じ
![操作イメージ](https://qiita-image-store.s3.amazonaws.com/0/348583/3a7c7e5f-eec1-258e-f707-239905c8c0ef.gif)

### ソースコード
http://obniz.io/program?root=/users/286/repo&filename=DoubleMotorCarController.html
（obniz Cloudのエディターが開きます。）

# 実走
![実走](https://qiita-image-store.s3.amazonaws.com/0/348583/888a5093-3c42-163f-c21f-142b2550707f.gif)
めっちゃタイヤ滑った…

# 感想
めっちゃ簡単にできた！すごい！！
~~本当はカメラモジュール買ったんで組み込んで遠隔操作できるようにしたかったんだけど、~~
~~動作確認がてら遊んでたらカメラモジュール壊れちゃった…~~
このあとで、ドットマトリックスLEDで遊んだし、
スマートリモコンもどき作ろうと思って、パーツ買ってあるのでまだまだ遊べそう。

[^1]: 本体価格にobnizクラウドの永久使用料も含まれてるらしい。
[^2]: Wi-fiもラジオだし間違っちゃない…ハズ