🧩

obniz x Kintone x sony MESHでドミノ倒しIoTしてみた

2022/09/01に公開

SONY MESHさんが技術資料を公開し、obnizのパーツライブラリにMESHが追加されました

これはなにか作ってみよう!ということで、MESHを見つめていると、ドミノ倒ししかイメージができなくなったので、MESHとobnizを使ってドミノ倒しをしてみました

登壇したときの資料はこちら

つくったもの

ドミノ倒しにMESHブロックを混ぜると、

  • ドミノ倒しタイムアタックをしたり
  • 遠隔でドミノ倒しの状況を知ったり
  • ドミノ倒しの記録をとったり

ができます!

(何の役に立つのかは不明)

つかったMESHブロック

MESHとドミノを混ぜても違和感ありません笑

ドミノといえばやはり倒れるもの、ということで倒れたことを検知するようにします。
倒れたことを検知するには、最初に思いつくのは 動きブロックです

https://meshprj.com/jp/products/blocks/MESH-100AC.html

動きブロックはそのまま、ブロックの姿勢を取ることができるので、シンプルにわかりやすくできます。
ただ、動きブロックをたくさん持っているわけではないのでこれだと1個だけになってしまいます。

それだと物足りないので、明るさブロックも使うことにしました。

https://meshprj.com/jp/products/blocks/MESH-100PA.html

倒れたら明るさのセンサ部分が暗くなるのでは!? ってことで使ってみたらちょうど良かったのでそのまま使ってます

obniz

今回は8月に発売されたばかりの obniz BLE/Wi-Fi Gateway Gen2.0を使いました
といっても、この機種特有の機能は使ってないのでobnizBoardでも動く(はず)です

obniz.js@3.23.0よりMESHブロックが使えるようになっています。

npm install obniz@3.23.0

kintone

せっかく計測するなら記録したいので、記録としてkintoneを使っています。
kintoneはデータも保存できるしグラフかもできるので便利ですね

開発者用のサンプルアカウントを作ることができるので、お試しにも最適です
https://developer.cybozu.io/hc/ja

プログラム

node.js/Typescript で書いています

大きく分けて

  • セットアップ(MESH↔obnizの接続)
  • ドミノ倒し実行
  • Kintoneへのアップロード
    の3つで構成しています。

セットアップ(MESH↔obnizの接続)

動きブロックと明るさブロックを見つけるまでずっとスキャン
→両方見つけたらそれぞれconnect
→セットアップ完了!

obniz.onconnect = async () => {
  log("obniz connected");
  await obniz.ble!.initWait();
  const SonyMeshFilter = [0x4d, 0x45, 0x53, 0x48, 0x2d, 0x31, 0x30, 0x30]; // MESH-100

  const MESH_100AC = Obniz.getPartsClass("MESH_100AC");
  const MESH_100PA = Obniz.getPartsClass("MESH_100PA");
  obniz.ble!.scan.onfind = async (peripheral) => {
    log("name:", peripheral.localName);
    if (!devices.ac && MESH_100AC.isDevice(peripheral)) {
      devices.ac = new MESH_100AC(peripheral);
    } else if (!devices.pa && MESH_100PA.isDevice(peripheral)) {
      devices.pa = new MESH_100PA(peripheral);
    }
    if (devices.ac && devices.pa) {
      await obniz.ble!.scan.endWait();
    }
  };

  await obniz.ble!.scan.startWait(
    { binary: [SonyMeshFilter] },
    { duration: null, filterOnDevice: true }
  );

  obniz.onloop = async () => {
    await wait(1000);
    if (obniz.ble!.scan.state === "stopped" && devices.ac && devices.pa) {
      await connectAndSetup();
      if (
        !devices.pa.peripheral.connected ||
        !devices.ac.peripheral.connected
      ) {
        return;
      }
    }
  };
};

ble.onfindの中でブロックを両方見つけたら、connectAndSetupを呼んでいます。

connectAndSetupでは、各デバイスの設定をしています

ドミノ倒し実行

センサからデータが来たら倒れているかチェック
→倒れていたらスタート! もしくはフィニッシュ!として記録しています

センサのデータが来たときのコールバックをきっかけに、スタートとフィニッシュの時間を記録しています。フィニッシュ時はkintoneにデータを送る sendToKintone() を呼んでいます。

const onMaybeStart = async () => {
  if (!measure.startUnixTime) {
    console.log("START!");
    measure.startUnixTime = new Date().getTime();
  }
};
const onMaybeFinish = async () => {
  if (measure.startUnixTime && !measure.finishUnixTime) {
    measure.finishUnixTime = new Date().getTime();
    const time = (measure.finishUnixTime - measure.startUnixTime) / 1000;

    console.log(`FINISH! ${time}ms`);
    await sendToKintone(time);
  }
};

Kintoneへのアップロード

記録をアップロードするだけですね

kintoneさんの公式SDKがあるのでそれを使っていきます。
ドキュメントも充実してるのでわかりやすかったです。


const { KintoneRestAPIClient } = require("@kintone/rest-api-client");

const client = new KintoneRestAPIClient({
  baseUrl: config.kintone.baseUrl,
  auth: {
    apiToken: config.kintone.apiToken,
  },
});

const sendToKintone = async (time: number) => {
  // 追加方法についてはこちら
  // https://developer.cybozu.io/hc/ja/articles/202166160-%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E7%99%BB%E9%8C%B2-POST-
  await client.record.addRecord({
    app: config.kintone.appId,
    record: {
      time: { value: time },
    },
  });
};

プログラム

プログラム全体はこちら


import Obniz, { Parts } from "obniz";
import MESH_100PA from "obniz/dist/src/parts/Ble/MESH_100PA";

const { KintoneRestAPIClient } = require("@kintone/rest-api-client");

const config = {
  kintone: {
    apiToken: "Kintoneのアプリトークン",
    baseUrl: "KintoneのURL",
    appId: 2, //kintoneのアプリID
  },
  obnizId: "obnizのID"
};

const client = new KintoneRestAPIClient({
  baseUrl: config.kintone.baseUrl,
  auth: {
    apiToken: config.kintone.apiToken,
  },
});

const sendToKintone = async (time: number) => {
  // 追加方法についてはこちら
  // https://developer.cybozu.io/hc/ja/articles/202166160-%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E7%99%BB%E9%8C%B2-POST-
  await client.record.addRecord({
    app: config.kintone.appId,
    record: {
      time: { value: time },
    },
  });
};

const obniz = new Obniz(config.obnizId);
const devices: {
  pa?: Parts<"MESH_100PA">;
  ac?: Parts<"MESH_100AC">;
} = {};

const measure: {
  startUnixTime: number | null;
  finishUnixTime: number | null;
} = { startUnixTime: null, finishUnixTime: null };

obniz.onconnect = async () => {
  log("obniz connected");
  await obniz.ble!.initWait();
  const SonyMeshFilter = [0x4d, 0x45, 0x53, 0x48, 0x2d, 0x31, 0x30, 0x30]; // MESH-100

  const MESH_100AC = Obniz.getPartsClass("MESH_100AC");
  const MESH_100PA = Obniz.getPartsClass("MESH_100PA");
  obniz.ble!.scan.onfind = async (peripheral) => {
    log("name:", peripheral.localName);
    if (!devices.ac && MESH_100AC.isDevice(peripheral)) {
      devices.ac = new MESH_100AC(peripheral);
    } else if (!devices.pa && MESH_100PA.isDevice(peripheral)) {
      devices.pa = new MESH_100PA(peripheral);
    }
    if (devices.ac && devices.pa) {
      await obniz.ble!.scan.endWait();
    }
  };

  await obniz.ble!.scan.startWait(
    { binary: [SonyMeshFilter] },
    { duration: null, filterOnDevice: true }
  );

  obniz.onloop = async () => {
    await wait(1000);
    if (obniz.ble!.scan.state === "stopped" && devices.ac && devices.pa) {
      await connectAndSetup();
      if (
        !devices.pa.peripheral.connected ||
        !devices.ac.peripheral.connected
      ) {
        return;
      }
    }
  };
};

const connectAndSetup = async () => {
  if (!devices.ac || !devices.pa) {
    return;
  }
  if (!devices.pa.peripheral.connected) {
    await devices.pa.connectWait();
    // Set event handler
    devices.pa.onSensorEvent = (proximity, brightness) => {
      if (!measure.startUnixTime) {
        console.log("proximity: " + proximity + ", brightness: " + brightness);
        if (proximity > 100) {
          onMaybeStart();
        }
      }
    };

    // Prepare params (See the linked page below for more information.)
    const notifyMode = MESH_100PA.NotifyMode.ALWAYS;

    // Write
    devices.pa.setMode(notifyMode);
    if (devices.pa.peripheral.connected && devices.ac.peripheral.connected) {
      console.log("Ready");
    }
  }

  if (!devices.ac.peripheral.connected) {
    await devices.ac.connectWait();
    devices.ac.onOrientationChanged = (orientation, accele) => {
      if (measure.startUnixTime && !measure.finishUnixTime) {
        console.log(
          "orientation changed! " +
            orientation +
            ", (ax, ay, az) = (" +
            accele.x +
            ", " +
            accele.y +
            "," +
            accele.z +
            ")"
        );
        if (![5, 2].includes(orientation)) {
          onMaybeFinish();
        }
      }
    };
  }
};

const onMaybeStart = async () => {
  if (!measure.startUnixTime) {
    console.log("START!");
    measure.startUnixTime = new Date().getTime();
  }
};
const onMaybeFinish = async () => {
  if (measure.startUnixTime && !measure.finishUnixTime) {
    measure.finishUnixTime = new Date().getTime();
    const time = (measure.finishUnixTime - measure.startUnixTime) / 1000;

    console.log(`FINISH! ${time}ms`);
    await sendToKintone(time);
  }
};

const onMaybeReady = async () => {};

const wait = (ms: number) => {
  return new Promise((resolve) => setTimeout(resolve, ms));
};
const log = (...args: any[]) => {
  console.log(new Date(), ...args);
};

log("start");

完成!

作ったら動かそうということで動かしました

3回しかやってないですが、ちゃんとkintoneにデータが来てました

Discussion