NURFの射撃支援システム作ってみた

NURFの弾道が山なりで的に当てづらいのでAtom Matrixでエイミングを支援するシステムを作ってみました

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">M5Sight(仮)ﾃﾞｷﾀｰ(ﾟ∀ﾟ)<br><br>NURFは弾道が山なりなのでLIDERで距離を測定して距離に従ってクロスを上下させて照準合わせを支援します<br><br>距離を測り終わったらAtomMatrixのボタンを押して距離測定を停止させてからクロスとフロントサイトを合わせて射撃します<a href="https://twitter.com/hashtag/M5Stack?src=hash&amp;ref_src=twsrc%5Etfw">#M5Stack</a> <a href="https://t.co/W7lJxoabCo">pic.twitter.com/W7lJxoabCo</a></p>&mdash; もけ@ムギ㌠ (@coppercele) <a href="https://twitter.com/coppercele/status/1658438317212581888?ref_src=twsrc%5Etfw">May 16, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## 使用するデバイス

M5 Atom Matrix
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/57bf186b-d7d1-b474-ad31-c1a5223a0897.png)

https://www.switch-science.com/products/6260?_pos=4&_sid=1d3280df8&_ss=r

小型のM5シリーズなら何でも良いのですが、
画面が必要なくて6軸センサが載っているのでAtom Matrixを使用しました。
6軸センサを使用した機能を使わないなら(後述)Atom liteやM5StickCなど手元にあるものを使えばいいと思います。

---

GLASS Unit
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/2e81a351-95eb-5e61-ee46-d1146b80833a.png)


https://www.switch-science.com/collections/m5stack/products/8866

透明なガラスに青い光が浮かび上がるクールな奴。
もともと射撃時に距離を測定して表示する方法を考えてて、
ダットサイトが理想的なんだけど作るの無理なんだよな～って思ってた時に見てコレダ！ってなって買いました。

---

LIDAR07 測距(ToF)モジュール
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/89a22dea-c5f3-4d64-aebc-4a30d07216a5.png)

https://wiki.dfrobot.com/TOF_IR_Distance_Sensor_0.2_12m_SKU_SEN0413

Amazonで売ってるToFモジュールの中で12mで4000円くらいとバランスが良かったのでこれを選びました。
残念ながら現在は売り切れです・・・

同じようなモジュールとしてはこちらがいいかもしれません
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/ddf715f6-e3c8-b857-b8a3-a6a273b0da90.png)

---

M5Stack用エンコーダユニット
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/54edca80-e802-9488-696a-cb8066dbd8cf.png)

https://www.switch-science.com/products/7987?_pos=1&_sid=0f9715fb9&_ss=r

クロスヘアを上下に調整するためにつけました。
調整が終わったら外しちゃっていいかも？

## 距離を測定する

LIDAR07 測距(ToF)モジュールのライブラリを使用します。

https://github.com/DFRobot/DFRobot_LIDAR07

サンプルからコードをつまむとこんな感じで使えます。

```cpp

#include "DFRobot_LIDAR07.h"

DFRobot_LIDAR07_IIC LIDAR07(Wire);
uint16_t distance = 0;

void setup(void) {

  Wire.begin(26, 32);
  M5.begin(true, false, false);
  LIDAR07.begin();
  LIDAR07.startFilter();
  LIDAR07.startMeasure();
  if (LIDAR07.getValue()) {
    distance = LIDAR07.getDistanceMM();
    Serial.printf("Distance:%6dmm ", LIDAR07.getDistanceMM());
  }
```

M5StackシリーズのあるあるとしてM5.begin()でI2C(Wire)を初期化すると
不具合が起きたりするのでM5.begin()と別で初期化した方がいいです。

## OLEDディスプレイを描画する

公式のサンプルコードを改変してスプライトを利用しています

m5-docs
https://docs.m5stack.com/en/unit/Glass%20Unit

M5Unit-GLASS/examples/Unit_GLASS_M5Atom/Unit_GLASS_M5Atom.ino at main · m5stack/M5Unit-GLASS · GitHub
https://github.com/m5stack/M5Unit-GLASS/blob/main/examples/Unit_GLASS_M5Atom/Unit_GLASS_M5Atom.ino

```cpp

#include <M5Atom.h>
#include <UNIT_GLASS.h>

M5UnitGLASS display;
LGFX_Sprite sprite(&display);

int width, height;
uint16_t distance = 9999;

void setup(void) {

  Wire.begin(26, 32);
  M5.begin(true, false, false);
  display.init(GPIO_NUM_26, GPIO_NUM_32, 400000u);

  display.setRotation(2);
  width = display.width();
  height = display.height();
  Serial.printf("width=%d,height=%d\n", width, height);

  sprite.createSprite(width, height);

  sprite.fillScreen(TFT_BLACK);

  sprite.setTextSize(1);
  sprite.setCursor(width / 2, height / 2);
  sprite.printf("%dmm",distance);
  sprite.pushSprite(0, 0);
}

```

これで画面の中央に距離が表示されます。

## 画面表示の内容を作りこむ

NURFは弾道が山なりなのでグレネードランチャーのサイトをイメージします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/275ffa14-a462-4aaa-2a66-d97c0b33d86c.png)

単純に距離に応じた目盛りを表示しても面白くないので、
距離に応じて十字が上下してフロントサイトと合わせて照準できるようにしたいと思いました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/80f3448f-c786-4db9-acc7-2c722a91e74a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/3a3587fa-0971-ba47-2389-219f3ccbd6fd.png)

全ての描画要素は距離、角度に応じて動かせるようにするためにwidth,heightを使って相対指定にしてあります。
(ただし正確ではない)

あと距離を7セグフォントで表示したかったのですが、
    spriteNum.setFont(&fonts::Font7);
&fonts::Font7は文字が大きくてUNIT GLASSの解像度には収まらないので、
数字表示用のspriteに描画してから縮小して合成しています。

```cpp
// setup()内で実行
  display.setRotation(2);
  width = display.width();
  height = display.height();
  Serial.printf("width=%d,height=%d\n", width, height);

  sprite.createSprite(width, height);
  spriteNum.createSprite(width * 2, height / 2);
  spriteNum.setCursor(0, 0);
  spriteNum.setPivot(width * 2, 0);

// 以下描画処理
// 左右の目盛り
    sprite.drawLine(0, 0, 10, 0, TFT_WHITE);
    sprite.drawLine(0, height * 0.2, 10, height * 0.2, TFT_WHITE);
    sprite.drawLine(0, height * 0.4, 10, height * 0.4, TFT_WHITE);
    sprite.drawLine(0, height * 0.6, 10, height * 0.6, TFT_WHITE);
    sprite.drawLine(0, height * 0.8, 10, height * 0.8, TFT_WHITE);
    sprite.drawLine(0, height, 10, height, TFT_WHITE);

    sprite.drawLine(width, 0, width - 10, 0, TFT_WHITE);
    sprite.drawLine(width, height * 0.2, width - 10, height * 0.2, TFT_WHITE);
    sprite.drawLine(width, height * 0.4, width - 10, height * 0.4, TFT_WHITE);
    sprite.drawLine(width, height * 0.6, width - 10, height * 0.6, TFT_WHITE);
    sprite.drawLine(width, height * 0.8, width - 10, height * 0.8, TFT_WHITE);
    sprite.drawLine(width, height, width, height, TFT_WHITE);

// 中央の十字
    sprite.drawLine(width / 2, height / 2, width / 2, height / 2 + 5, TFT_WHITE);
    sprite.drawLine(width / 2, height / 2, width / 2, height / 2 - 5, TFT_WHITE);
    sprite.drawLine(width / 2, height / 2, width / 2 - 10, height / 2, TFT_WHITE);
    sprite.drawLine(width / 2, height / 2, width / 2 + 10, height / 2, TFT_WHITE);

// 距離の表示
  spriteNum.setTextSize(1);
  spriteNum.setCursor(0, 0);
  spriteNum.setFont(&fonts::Font7);
  spriteNum.print("99.9");
// 距離描画用spriteを縮小してspriteに描画する
  spriteNum.pushRotateZoomWithAA(&sprite, width * 0.9, height * 0.02, 0, 0.35, 0.35);
  sprite.setCursor(width * 0.85, height * 0.05);
  sprite.setFont(&fonts::Font2);
  sprite.print("m");
  sprite.pushSprite(0, 0);

```

## 弾道計算をする

NURFはライフリングも無いし割と単純な放物線を描くだろう・・・という事で
放物線の式y=ax^2でザックリと計算しました
典型的なaは重力加速度の9.8のはずですが、
距離を測りながら水平射撃して大体実測に合うように調整しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/c60db34d-647c-92b4-2cf7-3bb68494811b.png)

本来は仰角(グラフは水平発射なので俯角になる)に合わせて＋記号を上下させようとおもってたのですが、
結局使わずにyで上下に動かすようにしました。

## 距離に応じてサイトを上下させる

距離に応じてディスプレイに反映します。

```cpp

 // 距離を取得
    if (canMeasure) {
      LIDAR07.startMeasure();
      if (LIDAR07.getValue()) {
        distance = LIDAR07.getDistanceMM();
        // Serial.printf("Distance:%6dmm ", LIDAR07.getDistanceMM());
      }
    }
  // 距離に応じた変化を計算y=ax^2(と比率調整)
    int y = 1.0 * pow((float)distance, 2) * 0.00001;
  // 距離に応じたサイトの変化量を計算
  // 5mで250mm下降するので最大とする
  // 250mm時に画面の高さheight*0.7分上に動かす
    int distanceShift = map(y, 0, 250, 0, -height * 0.7);

  // サイトの基準位置
  // xは画面中央yは画面の上から90%の位置を基準とする
  //　頬付けした場合大体その位置にフロントサイトが来る
    int sightX = width / 2;
    int sightY = height * 0.9;

  // sightYにdistanceShiftを足して上下に動かす
    sprite.drawLine(sightX, sightY + distanceShift, sightX - 10, sightY + 5 + distanceShift, TFT_WHITE);
    sprite.drawLine(sightX, sightY + distanceShift, sightX + 10, sightY - 5 + distanceShift, TFT_WHITE);
    sprite.drawLine(sightX, sightY + distanceShift, sightX - 10, sightY - 5 + distanceShift, TFT_WHITE);
    sprite.drawLine(sightX, sightY + distanceShift, sightX + 10, sightY + 5 + distanceShift, TFT_WHITE);

```

これにより＋サイトとフロントサイトを重ねて構えることで適正な俯角を取ることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/06e55c9b-b016-9bc4-1acd-e8ff4a37bf43.png)