季節外れのホワイトクリスマス

## ツリーの飾りつけ
我が家ではクリスマスツリーの飾りは、毎年一つづつくらい新しい飾りつけを追加している。

去年はLEDの点滅するものを新調した。そして、今年は少し工夫を凝らしたLEDと音楽を奏でるグッズを追加することにした。

まだ6月なので半年ほど早いが、直前になるとまた忘れてしまうので、早めに準備をするということと、今回はDIYなので、少しづつ完成度を高めていこうと思う。

## まずは、動かした様子から

https://x.com/tadokuno/status/1801231294778794470

手を近づけると、RGB LEDが点灯します。さらに近づけると、ホワイトクリスマスの音楽が流れます。そして、近づけたままだと1秒ごとにLEDの色が変わります。

# 制作のプロセス

## obnizでプロトタイプを作成

使用するものは、
- obniz Board 1Y
- 光センサ　CdSセル 5mmタイプ
- 超音波距離センサー HC-SR04
- 高精度IC温度センサ LM60BIZ
- マイコン内蔵RGBLED 8mm PL9823-F8
- 青色LED

<img width="70%"src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212141/841652ec-f8f4-758f-c408-bdbbee683e47.png">

これだけを使って、光と音のハーモニーを演出したい。

## 見る人を驚かせたい

ただ光ったり音が出るものではなく、見る人に驚きを与えたいと思った。
そのために、部屋に入ってツリーに近づいていくと、突然光って音を出し始めるというものを考えた。

距離センサーで距離を測って、一定の距離以下になると、LEDが光って音楽が鳴り出すというものにしたい。

## ハードウェア
昔の記事を参考にして、つないでみました。

https://qiita.com/PmanRabbit/items/371b2efebb7134efec9e


出来上がったものはこんな感じ

<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212141/54440616-311d-3388-5c46-1ae95e164cb3.jpeg">

<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212141/17ed82fa-c4a4-48a9-f104-333b32e11079.jpeg">

## 配線図
obniz は0から11の12ポートがあり、これに各デバイスを接続していく。
結線図は以下のとおり

| Port | デバイス | 意味 |
|------|-----------|------|
| 0 | スピーカー   | Signal |
| 1 | 共通   | GND |
| 2 | RGB LED | Din|
| 3 |  | |
| 4 | 距離センサ | Trig |
| 5 | 距離センサ | Echo |
| 6 |  | |
| 7 | 温度センサ | Output|
| 8 |  | |
| 9 | 共通 | Vcc|
| 10 | 光センサ | Output |
| 11 | 青色LED | アノード|

光センサは上記からは読み取れないが、センサと抵抗を直列につないで、間から電圧を測定している。obnizで出ている数字は電圧かどうかちょっとよく分からないが、室内の太陽光だと0.7程度の値が出ている。
配線は以下のような感じ。

<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212141/3a14ae0b-b07a-0b9f-3699-5864ccef0617.jpeg">

## ソフトウェア
プロトタイプなので、開発コンソールでサクッと作ってみた。

HTMLで簡単なUIを書いて、JavaScriptでデバイスとの入出力を記述している。

### シナリオ
- 電源をONにする。プログラムを実行する。
- Webでスタートボタンを押すと、青色LEDが1回点滅する。
- 部屋の電気を消すと青色LEDが点灯する。
- 人が1m以内に入ると、青色LEDが消え、RGB LEDがランダムの色で点滅する。
- さらに50cm以内になると、ホワイトクリスマスが流れる。

温度センサは、距離センサの補正に使用する。



## User Interface
- スタートボタン
- 設定：text field
  - 距離の設定：
    - LEDが点滅する距離(m) : dist1
    - 音楽が流れる距離(m): dist2
    - 測定間隔: (ms) これは設定せず、1000msに固定

- 表示
  - 温度: temp
  - 距離: distp
  - 照度: illum
  - RGB値 : rgb

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>クリスマスソング</title>
  <script src="https://obniz.com/js/jquery-3.2.1.min.js"></script>
  <script src="https://unpkg.com/obniz@3.30.0/obniz.js"></script>
  <style>
    .inline {
      display: inline;
    }
  </style>
</head>
<body>
    <h1>クリスマスツリー</h1>

    <label for="dist1">LEDが点灯する距離(m):</label>
    <input type="number" id="dist1" value="1.0" required><br>
    <label for="dist2">音楽が鳴る距離(m):</label>
    <input type="number" id="dist2" value="0.5" required><br>
    <label for="volto">照明の閾値(V):</label>
    <input type="number" id="volto" value="0.3" required><br>


    <label for="temp">気温</label>
    <div id="temp" class="inline">0.0</div> <br>
    <label for="distp">距離(m)</label>
    <div id="distp" class="inline">0.0</div> <br>
    <label for="illum">照度</label>
    <div id="illum" class="inline">0.0</div> <br>
    <label for="rgb">RGB値</label>
    <div id="rgb" class="inline">000 000 000</div>  <br>

    <button id="startTimer">開始</button>
<script>
... 処理を記述
</script>
</body>
</html>

```

Webでは、センサの値を見ながら、パラメータの変更をやりやすくしている。
初めて使うものなので、どの程度の精度なのか分からないし、obnizを通してどんな意味の値が出ているのかも判然としないので、値を見ながら調整する必要があると思ったからである。
ただ、Webを書いたのは久々だったので、ちょっと面倒くさいと思った。

UIはこんな感じ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212141/08667037-a2d2-0536-bc5d-8fb441b07901.png)


### ロジック　(JavaScript)

```javascript
const dist1Val = document.getElementById('dist1');
const dist2Val = document.getElementById('dist2');
const voltoVal = document.getElementById('volto');
const tempVal = document.getElementById('temp');
const distpVal = document.getElementById('distp');
const illumVal = document.getElementById('illum');
const rgbVal = document.getElementById('rgb');
const startBtn = document.getElementById('startTimer');

const obniz = new Obniz('Obniz_ID'); // Obniz_ID

// 任意の秒数待つことができる関数
// 参考: https://qiita.com/suin/items/99aa8641d06b5f819656
const sleep = (msec) => new Promise(res => setTimeout(res, msec));

// 音階
const Key = {
    "ド" : 261.626,
    "レ" : 293.665,
    "ミb" : 311.647,
    "ミ" : 329.628,
    "ファ" : 349.228,
    "ファ#" : 370.612,
    "ソ" : 391.995,
    "ラ" : 440.000,
    "シ" : 493.883,
    "ド2" : 523.251,
    "レ2" : 587.330
}

async function playMusic(speaker) {
    const unit = 600;
 
    speaker.play(Key["ミ"]); await sleep(unit*4); speaker.stop(); 
    speaker.play(Key["ファ"]); await sleep(unit); speaker.stop(); 
    speaker.play(Key["ミ"]); await sleep(unit); speaker.stop(); 
    speaker.play(Key["ミb"]); await sleep(unit); speaker.stop();
    speaker.play(Key["ミ"]); await sleep(unit); speaker.stop();
    speaker.play(Key["ファ"]); await sleep(unit*4); speaker.stop(); 
    speaker.play(Key["ファ#"]); await sleep(unit); speaker.stop(); 
    speaker.play(Key["ソ"]); await sleep(unit*4); speaker.stop(); 
    speaker.play(Key["ラ"]); await sleep(unit); speaker.stop(); 
    speaker.play(Key["シ"]); await sleep(unit); speaker.stop(); 
    speaker.play(Key["ド2"]); await sleep(unit); speaker.stop(); 
    speaker.play(Key["レ2"]); await sleep(unit); speaker.stop(); 
    speaker.play(Key["ド2"]); await sleep(unit); speaker.stop(); 
    speaker.play(Key["シ"]); await sleep(unit); speaker.stop(); 
    speaker.play(Key["ラ"]); await sleep(unit); speaker.stop(); 
    speaker.play(Key["ソ"]); await sleep(unit*5); speaker.stop(); 
}


async function measureDist(hcsr04) {
    let avg = 0;
    while(true) {
      let count = 0;
      for (let i=0; i<3; i++) { // measure three time. and calculate average
        const val = await hcsr04.measureWait();
        if (val) {
          count++;
          avg += val;
        }
      }
      if (count > 0) {
        avg /= count;
        console.log(avg);
//        distpVal.textContent = `${avg} mm`;
        break;
      }
      await obniz.wait(100);
    }
    return avg;
}

async function getRandomInt(min, max) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min + 1)) + min; // The maximum is inclusive and the minimum is inclusive
}

async function setRandomRGB(rgbled) {
  const r =  await getRandomInt(1, 100);
  const g =  await getRandomInt(1, 100);
  const b =  await getRandomInt(1, 100);
  rgbled.rgb(r,g,b);
  rgbVal.textContent = `${r} ${g} ${b}`;
}

obniz.onconnect = async function () {

    // ディスプレイ表示
    obniz.display.clear();
    obniz.display.print('Start');

    // 初期化
    // スピーカー
    const speaker = obniz.wired('Speaker', { signal: 0, gnd: 1 });
    speaker.play(Key["ソ"]); await sleep(500); speaker.stop(); await sleep(100);

    obniz.io9.output(true);  // io9電圧を5Vに（電源Vcc）

    // 光センサ
    var voltage = await obniz.ad10.getWait();
    illumVal.textContent = `${voltage} v`;

    // 青色LED
    const led = obniz.wired('LED',{anode:11, cathode:1});
    led.on();
    await sleep(100);
    led.off();

    // RGB LED
    const rgbled = obniz.wired('WS2811', { gnd: 1, vcc: 9, din: 2 });
    rgbled.rgb(100,100,100) // 点灯
    await sleep(100);
    rgbled.rgb(0,0,0) // 消灯

    // 温度センサ
    const tempsens = obniz.wired("LM60", { gnd:1 , output:7, vcc:9});
    var temp = await tempsens.getWait();
    tempVal.textContent = `${temp} ℃`

    // 距離センサ
    const hcsr04 = obniz.wired("HC-SR04", {gnd:1, echo:5, trigger:4, vcc:9});
    hcsr04.temp = temp;
    var dist = await measureDist(hcsr04);
    distpVal.textContent = `${dist} mm`;

    // 初期化終わり
    obniz.display.clear();
    obniz.display.print('Initialzed!');
    led.off();
    rgbled.rgb(0,0,0) // 消灯

    startBtn.onclick = async function() {
      // setIntervalで一定間隔にセンサーからの入力を取得する

      led.on();
      await sleep(500);
      led.off();

      var flag = 0;

      setInterval(async function () {
        // 比較するデータを取得する。
        var dist1num = parseFloat(dist1Val.value) * 1000; // (mm)
        var dist2num = parseFloat(dist2Val.value) * 1000; // (mm)
        var voltonum = parseFloat(voltoVal.value);

        // io10をアナログピンに(照度センサーの値を取得)
        voltage = await obniz.ad10.getWait();
        illumVal.textContent = `${voltage} v`;
        console.log(`changed to ${voltage} v`);

        if (voltage < voltonum) { // 暗くなった時
          led.on();
        } else {
          led.off();
        }


        var dist = await measureDist(hcsr04);
        console.log(`distance: ${dist} mm`);
        distpVal.textContent = `${dist} mm`;
        if( dist2num < dist && dist < dist1num ) {
	  led.off();
          await setRandomRGB(rgbled);
        }
        else if( dist < dist2num ) {
          if( flag ) {
            return;
          }
          console.log('playMusic')
          flag = 1;
          await playMusic(speaker);
          flag = 0;
        } else {
          led.on()
          rgbled.rgb(0,0,0);
        }
      }, 1000);  // 1秒
    }
}
```
ちょっと長くなってしまったが、順番に説明する。

最初の10行は、画面入出力のためのオブジェクトの取得と、obnizのオブジェクトの取得で、そのあと必要な関数を定義している。

sleep(msec)
　msecミリ秒ウエイトを掛ける
playMusic(speaker): 
　speakerに対して音楽をだす。音階は音の周波数で指定できる。ここで定義している音楽はホワイトクリスマスです。

この辺りは参考にさせていただきました。

https://qiita.com/PmanRabbit/items/371b2efebb7134efec9e

measureDist(hcsr04):
　距離センサから距離を取得します。

getRandomInt(min,max):
　乱数を生成します。これは、RGB LEDのRGBの値をランダムに決めるために準備した関数です。

setRandomRGB(rgbled):
　RGB LEDの値をセットして、UIにも表示する関数です。

そして、obnizが接続された際に呼び出す関数を、obniz.onconnectに定義しています。
ここでは、センサーの初期化などを順次行っています。
そして、UIの開始ボタンを押すと、startBtn.onclickとして定義された関数を呼び出します。ここで、センサの値を1秒おきにセンスしながら、値に応じて処理を行っていきます。

## 実行！！

最初に動画は出しましたが、もう少しアップにしたものも撮りましたので、出しておきます。


https://x.com/i/status/1800491276221182220

ちょっと動画が長すぎですね。

## やってみて

最初何気にクリスマスツリーをお題に始めたのですが、これホントによいと思いました。
小型化して、LEDをたくさん付けて、今年のクリスマスに向けて量産したいです。LEDは安いですしね。obnizが一番コスト高ですが、もっと簡易で安いコントローラーは無いのでしょうか。原価は千円以下にしないと商品にはならないかなとは思います。

去年買った、しょうもないLEDの色が変わって点滅するだけのものは、ハードオフで1000円でした。売値は多分1万円以上するものだと思います。無駄にGoogle Homeに対応してたりするので。それよりも良いですね。

## 改善すべきところ

超音波センサーの指向性が高いので、ずれて近づくと上手く動きません。また、距離の値も信頼性が無さそうです。距離の測定については改善の余地がありそうですし、もっと別の方法で近づいていることをセンスする方法があれば嬉しいですね。

以上です。