Obniz Cloudとnipple.jsを連携したらメカ操作が楽になった

# はじめに
Obnizを使ったラジコンカーを微調整操作をする際にいいUIがなくて困っていたところ、nipple.jsというイマドキなジョイスティックっぽい操作ができるパッケージを知った。そんなわけで連携してみた話です。

# 作ったもの(結果)
nipple.jsを用いて実装されたUIによってゆるゆるバギーが走行しています。
ポイントとしては、速度の強弱や角度の微調整が行えているところです。

<iframe width="560" height="315" src="https://www.youtube.com/embed/4u3C2es7pas" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

### ゆるゆるバギー作成に使ったもの
- [Obniz Board 1Y](https://akizukidenshi.com/catalog/g/gM-14930/)
- [finger pow 2](https://fingerpow.co/)　※今は販売を停止している模様。代用であればこれとかいいかも？ ：https://www.amazon.co.jp/dp/B09DSY3HSZ/ref=cm_sw_r_tw_dp_PVVGMBPNTKC2VY92WAQF
- [赤GeekServo](https://www.switch-science.com/catalog/6812/)(2線のギアモーター) 2個
- [灰色GeekServo](https://www.switch-science.com/catalog/6811/)(３線の角度サーボモーター) 2個
- LEGO TechnicのLiftarmとかpegとか色々
- 輪ゴムとホッチキスの針 それぞれ2つ

ちなみに動きとしてはこんなかんじです。

<iframe width="560" height="315" src="https://www.youtube.com/embed/40S1t4MYNZs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# nipple.jsについて
公式ページはこちら：https://yoannmoi.net/nipplejs/

nipple.jsとは、[yoann moinet氏](https://github.com/yoannmoinet)が主体になって作成された、タッチ操作を考慮した仮想ジョイスティックインターフェースです。

公式サイトで示すとおり、比較的文量の少ないコードでタッチ圧力などの細かい操作まで取得することができます。マルチタップもできるので非常に高機能です。

<iframe width="560" height="315" src="https://www.youtube.com/embed/hIg3_8EacP4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

ちなみにnipple.jsはCDNでも提供されています。Obniz Cloudを利用する都合から、今回はCDNを使用しました。
https://cdnjs.com/libraries/nipplejs

# 今回の実装について
実装したコードは主に三つに分けられます。

- nipple.js使用領域の用意
- nipple.jsの初期化＆イベント取得処理の実装
- スティック位置を取得＆モーター駆動コードを書く。

## nipple.js使用領域の用意
nipple.jsを使うためには、さきに画面をタッチ（クリック）した際にジョイスティックを表示・操作する領域を作成する必要があります。今回の実装では左右で別々のジョイスティックを使いたかったため、このような画面を作成しました。
(注意：Obniz Cloud上で実装した都合、bootstrap　4.3.1を使用しています。)

```html:ui
  <div class="container">
    <div class="row">
      <div id="zone_joystick_r" style="width:100%;height:600px;background-color:skyblue;" class="rounded col">
        dynamicR<br>アクセル</div>
      <div id="zone_joystick_l" style="width:100%;height:600px;background-color:pink;" class="rounded col">dynamicL<br>ステア
      </div>
    </div>
  </div>
```

<img width="600px" alt="画面領域の例" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271381/7684d6e6-a813-26c8-c588-a196f78f945f.png">

## nipple.jsの初期化＆イベント取得処理の実装
nipple.js初期化時に上記領域のid(zone_joystick_r,zone_joystick_l)を引数zoneに指定することで、ジョイスティックを表示する領域を指定することができます。
左右でジョイスティックの色を変える際には、colorを引数にわたすことで指定できます。

```javascript:init
var dynamicR = nipplejs.create({
  zone: document.getElementById("zone_joystick_r"),
  color: "blue",
});
var dynamicL = nipplejs.create({
  zone: document.getElementById("zone_joystick_l"),
  color: "red",
});
```

大抵の実装では、joystickが操作された際になにかの処理が動いてほしいと考えるかと思います。今回は[公式サンプルのcodepen](https://codepen.io/YoannM/pen/gapmMG)の内容を参考にして下記コードを実装しました。

```javascript:joystickevent
  bindNipple();

  function bindNipple() {
    dynamicR
      .on("start end", function (evt, data) {
        // joystick操作開始・終了時呼び出し
      })
      .on("move", function (evt, data) {
        // joystickを動かしたとき呼び出し
      })
      .on(
        "dir:up plain:up dir:left plain:left dir:down " +
          "plain:down dir:right plain:right",
        function (evt, data) {
          // joystick指はなし時呼び出し
        }
      )
      .on("pressure", function (evt, data) {
        //  joystickに圧がかかっているとき呼び出し
      });
  }
```

onメソッドに任意のイベントを入れることで、細かいイベント別の処理を走らせることが可能です。
今回のObnizゆるバギーの走行にはジョイスティックのスティック移動と開始・終了時の処理呼び出しを使います。


## スティック位置を取得＆モーター駆動コードを書く。
joystickの情報は上記イベントメソッドの引数evtから取得可能です。
今回はジョイスティックの位置情報がほしかったため、このようにデータを取得しました。

```javascript:stickmove
evt.target.nipples[0].frontPosition.x
evt.target.nipples[0].frontPosition.y
```
evt.target.nipples[0].frontPosition.xは水平方向のジョイスティック位置であり、無操作時を0として左一杯にうごかしたときに+50,右一杯にうごかして-50という値が入ります。同様に、evt.target.nipples[0].frontPosition.yは垂直方向のジョイスティック位置で、無操作時に0、上一杯操作で+50,下一杯操作で-50という値が入ります。

ちなみに同じ画面領域で複数のジョイスティックを出すことができるため、２つ以上のジョイスティックを操作する際にはnipples[1]のようにインデックスを増やすことで解決できます。

値の正負をモーターの制御に用いるべく、ゆるバギーの前後動作にはこのような関数を実装しました。powにはevt.target.nipples[0].frontPosition.yがそのまま入ります。
(pwm1はモーター正転側、pwm2はモーター逆転側の強弱付けのためのpwmオブジェクトです。)

```javascript:front-back
  function flipPWM(pow) {
    if (pow >= 0) {
      pwm1.duty(pow);
      pwm2.duty(0);
      return;
    }
    pwm1.duty(0);
    pwm2.duty(-pow);
  }
```
最後に、サーボの操作はこのように実装しました。ちなみに95.0はサーボの中央位置です。

```javascript:servo
servo.angle(95.0 + evt.target.nipples[0].frontPosition.x / 2);
```

## コード全体
ボタン操作はデバッグ用です。

```javascript:allcode.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" />
  <link rel="stylesheet" href="/css/starter-sample.css" />
  <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
  <script src="https://unpkg.com/obniz@3.6.1/obniz.js" crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/nipplejs/0.9.0/nipplejs.min.js"
    integrity="sha512-7PRZndBOTMkYqbGwO6dvNDozKEwaJYu3zPLoLv0rzOLMPQ2PVh6yaevCZOUL8+/n+A16fnLYEBKAlYXgMBn54w=="
    crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>

<body>
  <div class="container">
    <div class="row">
      <div id="zone_joystick_r" style="width:100%;height:600px;background-color:skyblue;" class="rounded col">
        dynamicR<br>アクセル</div>
      <div id="zone_joystick_l" style="width:100%;height:600px;background-color:pink;" class="rounded col">dynamicL<br>ステア
      </div>
    </div>
  </div>


  <div class="led">
    <button class="btn btn-primary" id="F">Forward</button>
    <button class="btn btn-primary" id="B">Back</button>
    <button class="btn btn-primary" id="R">Right</button>
    <button class="btn btn-primary" id="L">Left</button>
  </div>

  <script>
    var dynamicR = nipplejs.create({
  zone: document.getElementById("zone_joystick_r"),
  color: "blue",
});
var dynamicL = nipplejs.create({
  zone: document.getElementById("zone_joystick_l"),
  color: "red",
});

var obniz = new Obniz("YOUR OBNIZ ID");

obniz.onconnect = async function () {
  var pwm2 = obniz.getFreePwm();
  pwm2.start({ io: 11 });
  pwm2.freq(10000); // 10k hz
  pwm2.duty(0);

  var pwm1 = obniz.getFreePwm();
  pwm1.start({ io: 10 });
  pwm1.freq(10000); // 10k hz
  pwm1.duty(0);

  var servo = obniz.wired("ServoMotor", { gnd: 4, vcc: 5, signal: 6 });
  servo.angle(95.0);

  bindNipple();

  function bindNipple() {
    dynamicR
      .on("start end", function (evt, data) {
        // console.log("evt st:",JSON.stringify(evt))
        flipPWM(0);
      })
      .on("move", function (evt, data) {
        // console.log("evt move:",JSON.stringify(evt))
        console.log("evt move:", evt.target.nipples[0].frontPosition.y);
        flipPWM(evt.target.nipples[0].frontPosition.y * 2);
      })
      .on(
        "dir:up plain:up dir:left plain:left dir:down " +
          "plain:down dir:right plain:right",
        function (evt, data) {
          // console.log("evt up:",JSON.stringify(evt))
        }
      )
      .on("pressure", function (evt, data) {
        //  console.log("evt press:",JSON.stringify(evt))
      });

    dynamicL
      .on("start end", function (evt, data) {
        // console.log("evt st:",JSON.stringify(evt))
        servo.angle(95.0);
      })
      .on("move", function (evt, data) {
        // console.log("evt move:",JSON.stringify(evt))
        console.log("evt move:", evt.target.nipples[0].frontPosition.x);
        servo.angle(95.0 + evt.target.nipples[0].frontPosition.x / 2);
      })
      .on(
        "dir:up plain:up dir:left plain:left dir:down " +
          "plain:down dir:right plain:right",
        function (evt, data) {
          // console.log("evt up:",JSON.stringify(evt))
        }
      )
      .on("pressure", function (evt, data) {
        //  console.log("evt press:",JSON.stringify(evt))
      });
  }

  function flipPWM(pow) {
    if (pow >= 0) {
      pwm1.duty(pow);
      pwm2.duty(0);
      return;
    }
    pwm1.duty(0);
    pwm2.duty(-pow);
  }

  await $("#F").on("touchstart mousedown", async () => {
    pwm1.duty(0);
    pwm2.duty(100);
  });
  await $("#F").on("touchstart mouseup", async () => {
    pwm1.duty(0);
    pwm2.duty(0);
  });
  await $("#B").on("touchstart mousedown", async () => {
    pwm1.duty(100);
    pwm2.duty(0);
  });
  await $("#B").on("touchstart mouseup", async () => {
    pwm1.duty(0);
    pwm2.duty(0);
  });

  await $("#R").on("touchstart mousedown", async () => {
    servo.angle(105.0 + 10);
  });
  await $("#R").on("touchstart mouseup", async () => {
    servo.angle(95.0);
  });
  await $("#L").on("touchstart mousedown", async () => {
    servo.angle(85.0 - 10);
  });
  await $("#L").on("touchstart mouseup", async () => {
    servo.angle(95.0);
  });
};

  </script>
</body>

</html>
```


# まとめ
nipple.jsを使うことで、メカ操作を微調整できるようになった。ハードウェアをAPI的につかえるObnizはモダンなWebUIとすぐ組み合わせられて良いですね。