LoginSignup
6
5

More than 3 years have passed since last update.

#Unity のオブジェクトと #toio を IoTの仕組み(MQTT)でリアルタイムに連動させる仕組み( #ロボやろ )

Last updated at Posted at 2020-11-29

この記事は、下記の toio のコンテストに Unity超初心者がエントリーして、コンテスト用の作品の試作を行った過程を記事にしたものです。

●【追記:審査員&制作期間延長決定!】本日10/10エントリー開始!「ロボットやろうぜ!- toio & Unity 作品動画コンテスト -」 | toio blog | toio(トイオ)
 https://toio.io/blog/detail/20201010-1.html
ロボやろ.jpg

試作した内容は、IoT関連の話でよく登場する MQTT を使い、Unity内のオブジェクトと現実世界の toio の動きを連動させる、というものです。以下のツイートや、YouTube に動画をアップしています。

●toio と Unity の位置座標連動の仕組みを試作してみた( #toio #Unity #ロボやろ ) - YouTube
 https://www.youtube.com/watch?v=Fic0unaSpw0

そして、 #ヒーローズリーグM5Stack のコンテスト等でも利用され、また自分が作った作品を登録することもできる ProtoPedia(プロトペディア)への登録も行ってみました。

●toioとUnityの位置座標連動の仕組みを試作してみた( #toio #Unity #ロボやろ ) | ProtoPedia
 https://protopedia.net/prototype/1989

全体構成

冒頭で動画を掲載していた今回の試作内容について、全体構成は以下のとおりです。

構成図(toio、Unity).jpg

そして、おおまかな処理の流れは以下となります(※ 処理の流れの中に書いてある用語等は、その後の部分で補足していきます)。

  1. toio が開発用プレイマット上に置かれた状態で位置が変わると、その位置座標がリアルタイムに BLE 通信で PC へと送られる
  2. PC上で動いているプログラム(JavaScriptによる実装)が toio の位置座標を受信し、その座標の情報を PC上の MQTTブローカーへ送信する
  3. MQTTブローカーは JavaScript で実装されたプログラムから受信した位置座標の情報を、Unity の C# で実装されたプログラムへ送る
  4. C# で実装されたプログラムは、受信した toio の位置座標の値に応じて、Unity内のオブジェクトの位置を変更する

以下で、上記の流れの個々の項目について、概要を記載します。

toio と Unity の間の通信

冒頭で、toio と Unity の間を MQTT を使ってつないでいると書きましたが、その MQTT を用いた通信では「MQTTブローカー」と呼ばれる仲介役が必要です。
この MQTTブローカーを提供しているクラウドサービス(クラウド版の shiftr.io 等)もあるのですが、インターネットを介すると遅延が発生するため、今回の構成ではローカル環境に自前で MQTTブローカーをたてました(詳細は後述しますが、Mosquitoデスクトップ版の shiftr.io の 2つを試しています)。

toio の制御の部分

toio と PC の通信には BLE を用い、その処理は Web Bluetooth API等を利用した HTML+JavaScript の上で実装しています。今回の仕組みに Web Bluetooth API を利用した理由ですが、これは自分が過去に作品を作った際のノウハウがあったためです(その際、機械学習を使う「Teachable Machine」での実装には HTML+JavaScript を使うのが都合が良かった、という背景がありました)。

上記の動画は過去の toio のキャンペーンの際に出したものですが、この動画でも使っている toio の制御やその前段階のペアリングの仕組みは、その前後で「Tsukuba Mini Maker Faire 2020」・「Maker Faire Tokyo 2020」の出展用に作った作品等でも使っていました。
そして、開発用プレイマット と toio を組み合わせた場合の toio との間での情報のやりとりを含め、以下のような記事としてノウハウを残していました。

さらに、JavaScript のプログラムには、toio から受信した位置座標の情報を、MQTTブローカーへ送信する処理も実装しています。

Unity側の処理の実装

Unity側で MQTT を扱う部分とオブジェクトの位置を変更する部分のプログラムは、C# で実装しています。

冒頭にも書いたとおり、自分が Unity の超初心者であるため、コンテスト用の試作を行うにあたり Unity側の仕組みを作り始める部分は悩ましいところでした。これについては、事前調査を行った結果 Unity の公式チュートリアルの 1つを活用して作るのが良さそうだと思われました。

具体的には、以下の「玉転がし(Roll-a-Ball)」のチュートリアルを途中まで進めたものを使いました。
C# のコードが出てくるのは以下の 1つ目のチュートリアルですが、壁や床を作ったり等のオブジェクトを取り扱う部分は以下の 2つ目のチュートリアルも共通で、そちらのほうが見やすかったため、そちらを用いました。

●玉転がし - Unity Learn
 https://learn.unity.com/project/yu-zhuan-gashi?language=ja

●Unity入門チュートリアル 「玉転がし」(Roll-a-Ball) ビジュアルスクリプティング版 - Unity Learn
 https://learn.unity.com/project/bolt-roll-a-ball-tutorial

玉転がし_-_Unity_Learn.jpg

ここから、今回の実装の詳細について補足していきます。

構成要素の詳細

以下で、MQTT関連・toio関連・Unity関連のそれぞれの部分について、詳細を記載していきます。
まずは、toio・Uniy の実装にも登場する MQTT の部分について説明します。

MQTTブローカーに関する部分

今回、自分は MQTT による通信の仲介役となる MQTTブローカーとして、Mac上でデスクトップ版の shiftr.io を用いるパターンと Mosquito を用いるパターンの 2つを試しました。
どちらか一方のみ用いれば良いのですが、それぞれを利用する方法について簡単に補足します。

デスクトップ版の shiftr.io を使う

今まで、無償利用も可能な MQTTブローカーを提供していた shiftr.io が、最近、デスクトップ版のアプリをリリースしました。詳細は以下の記事にも書いたのですが、これを使う方法について大まかな内容を記載します。

●MQTTブローカー shiftr.io の新バージョンをチェックしてみる! 〜デスクトップアプリ編〜 - Qiita
 https://qiita.com/youtoy/items/8cbc9e6fa1cd46ca2c3c

利用手順は簡単で、以下のとおりです。

  1. デスクトップ版の shiftr.io のページの下部より、デスクトップ版アプリをダウンロード(※ Windows/Mac/Linux用の 3つがあります)
  2. アプリをインストールし起動
  3. アプリを起動した状態で MQTT の処理を実行

1つだけ注意点があり、上記 2)で起動したアプリ(以下の図のもの)は、接続用のポート番号として「1883/1884」の 2つが用意され、場合によっては使い分ける必要がでてきます。
shiftr_io_Desktop.jpg

それぞれ、「MQTT over TCP」用と「MQTT over WebSocket」用になるのですが、今回はその両方を使い分けて利用することになります。とりあえずは、これ以降に出てくる toio用の処理(JavaScript実装)では「1884」、Unityの処理(C#実装)では「1833」と覚えておいてください。

Mosquito を使う

Mac で Mosquito をインストールする際には、Homebrew を用いました(Homebrew の導入方法については公式ページの説明等をご覧ください)。具体的なインストール用のコマンドは brew install mosquito となります。

これを実行する場合、 mosquito というコマンドのみでも起動できるのですが、それだと「MQTT over TCP」用のポート番号「1883」だけしか利用できません。デスクトップ版の shiftr.io の説明で書いていた「MQTT over WebSocket」用の通信も今回は行う必要があるため、もう1手間必要です。

テキストエディタ等で、以下の内容のコンフィグファイルを作成してください。ファイル名は何でも良いのですが、ここでは「mosquitto.conf」という名前とします。

mosquitto.conf
listener 1883

listener 1884
protocol websockets

そして、Mosquito の実行時にオプションを指定します。具体的には、 mosquito -c mosquitto.conf となります。これで、toio用の処理(JavaScript実装)で用いる「MQTT over WebSocket(ポート番号 1884)」と、Unityの処理(C#実装)で用いる「MQTT over TCP(ポート番号 1833)」の両方が利用可能な状態で Mosquito が立ち上がります。

toio関連の部分

HTML+JavaScript による実装

toio に関する部分は、「toio との通信(BLE での接続、マット上の位置座標を取得)」と「MQTT による通信」を主に実装します。HTML+JavaScript の上で実装したと記載していましたが、具体的なソースコードは以下となります(簡単のため、今回はファイルを分けず 1ファイルで作成しました)。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MQTT で toio の位置座標を送る</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
<script src="https://unpkg.com/mqtt@4.1.0/dist/mqtt.min.js"></script>
</head>

<body>
  <section class="section">
    <div class="container">
      <h1 class="title is-5">
        toio用のボタン
      </h1>
      <div class="buttons">
        <button id="b1" class="button is-success is-light">接続</button>
        <button id="b2" class="button is-danger is-light">切断</button>
      </div>
</section>

<script>

const MQTTURL = 'ws://127.0.0.1:1884'; //ローカル環境で用意した MQTTブローカーの接続先指定(ポート番号 1884)
const TOPIC = 'test'; // MQTT のトピックは Unity側と合っていれば何でも良い
let options = {
  clientId: 'Browser-'+Math.floor(Math.random() * 100) // MQTT のクライアント ID を生成
};

let client = mqtt.connect(MQTTURL, options); // MQTTブローカーへ接続

// MQTTブローカーに接続された場合に実行される処理
client.on('connect', function(){
  console.log('MQTTブローカーに接続完了');
  // 今回は必須ではないが、MQTT の Subscribe の処理も実装しておく
  client.subscribe(TOPIC, function(err, granted) {
    if (err) {
      console.log('subscribe の処理に失敗:', err);
    } else {
      console.log('subscribe の処理に成功');
    }
  });
});

// MQTTブローカーからメッセージを受信した際に実行される処理
client.on('message', function(topic, message){
    console.log('subscribe ⇒ ', 'トピック:', TOPIC, 'メッセージ:', message.toString());
});

// MQTTブローカーにメッセージを送るための処理
function pubMQTT(message) {
  client.publish(TOPIC, message);
  console.log('publish:メッセージ ⇒ ' + message);
}

// ライブラリを使わずボタン押下時の処理を紐付ける設定を行う
const b1 = document.getElementById("b1");
const b2 = document.getElementById("b2");
b1.addEventListener("click", function(){onConnectButtonClick(cube)}, false);
b2.addEventListener("click", function(){onDisconnectButtonClick(cube)}, false);

// BLE通信用: toio関連の UUID(toio との接続用+座標情報の受信用)
const TOIO_SERVICE_UUID          = '10b20100-5b3b-4571-9508-cf3efcd7bbae'; // 仕様: https://toio.github.io/toio-spec/docs/ble_communication_overview
const ID_SENSOR_CHARACTERISTICS_UUID = '10b20101-5b3b-4571-9508-cf3efcd7bbae'; // 仕様: https://toio.github.io/toio-spec/docs/ble_sensor

// 今後、複数の cube を利用する際にあると便利かもしれない部分
const cube = {
  deviceCube: undefined,
  idSensorChar: undefined
};

// BLE で toio に接続するための処理
async function onConnectButtonClick(cube) {
  let serviceUuid = TOIO_SERVICE_UUID;
  let characteristicUuid;

  try {
    console.log('Bluetoothデバイスの検索開始');
    cube.deviceCube = await navigator.bluetooth.requestDevice({
        // toio のみをスキャン結果に出すためのフィルター
        filters: [{services: [serviceUuid]}]
    });
    console.log('接続処理1');
    const server = await cube.deviceCube.gatt.connect();
    const service = await server.getPrimaryService(serviceUuid);

    console.log('接続処理2:座標情報を受け取るための処理');
    cube.idSensorChar = await service.getCharacteristic(ID_SENSOR_CHARACTERISTICS_UUID);
    // マット上の座標情報などが toio から通知されるように設定
    await cube.idSensorChar.startNotifications();
    console.log('通知を開始');
    cube.idSensorChar.addEventListener('characteristicvaluechanged',
        handleNotifications);
  } catch(error) {
    console.log('接続処理に失敗! ' + error);
  }
}

// toio からの通知を受け取った場合に実行される処理
function handleNotifications(event) {
  let value = event.target.value;

  // 受信したバイナリのデータから情報を座標情報を読み取る
  // 関連する仕様: https://toio.github.io/toio-spec/docs/ble_id
  if(value.getUint8(0).toString(16)==='1') {
    outputReadValue(value); // デバッグ用にログを出力
    console.log("キューブの中心の X "+value.getUint16(1, true)); // リトルエンディアン、キューブの中心の X 座標値
    console.log("キューブの中心の Y "+value.getUint16(3, true)); // リトルエンディアン、キューブの中心の Y 座標値
    pubMQTT(value.getUint16(1, true) + "," + value.getUint16(3, true)); // MQTT で座標情報を送信する
  } else if(value.getUint8(0).toString(16)==='3'){
    console.log('マットの上にのってないよ');
  }
}

// toio の切断処理をするためのもの
function onDisconnectButtonClick(cube) {
  if (!cube.deviceCube) {
    return;
  }
  console.log('Bluetoothデバイスを切断');
  if (cube.deviceCube.gatt.connected) {
    cube.deviceCube.gatt.disconnect();
  } else {
    console.log('既に切断済みです');
  }
}

// デバッグ用情報をまとめて出力するための処理
// 関連する仕様: https://toio.github.io/toio-spec/docs/ble_id
function outputReadValue(readValue) {
  console.log("キューブの中心の X "+readValue.getUint16(1, true)); // リトルエンディアン、キューブの中心の X 座標値
  console.log("キューブの中心の Y "+readValue.getUint16(3, true)); // リトルエンディアン、キューブの中心の Y 座標値
  console.log("キューブの角度 "+readValue.getUint16(5, true)); // リトルエンディアン、キューブの角度
  console.log("読み取りセンサーの X "+readValue.getUint16(7, true)); // リトルエンディアン、読み取りセンサーの X 座標値
  console.log("読み取りセンサーの Y "+readValue.getUint16(9, true)); // リトルエンディアン、読み取りセンサーの Y 座標値
  console.log("読み取りセンサーの角度 "+readValue.getUint16(11, true)); // リトルエンディアン、読み取りセンサーの角度
}

</script>
</body>
</html>

HTML の部分は、ヘッダ内で「bulma.min.css」と「mqtt.min.js」を読み込み、ボディの中では 2つのボタンを用意しています。

「bulma.min.css」は見た目を整えるために読み込んだ、CSSフレームワークの「Bulma」です。また「mqtt.min.js」は MQTT の通信処理を行うためのライブラリである「MQTT.js」です。

ボタン 2つを用意するところの周りで、class に section や container 等が指定されていますが、これは Bulma に関わる部分です。今回の本質的な部分ではないため、ここでは詳細説明は省きますが、気になる方は公式のドキュメントや Web で検索して出てくる記事などをご参照ください。

また、JavaScriptで実装された内容については、ソースコード内のコメントを参照ください。

使い方

上記のファイルを利用する際は、ブラウザ上で開いてください。ローカルに置いているファイルをそのまま開く形で OK です。

以下は Google Chrome で開いた際の表示例です。

Chromeでファイルを開いた状態.jpg

このページの「接続ボタン」を押す前に、 toio の電源を ON にしてください。
toio の電源が入り、接続処理を行えるようになった状態でこの「接続ボタン」を押します。

そうすると、以下のような表示が出てくるので、以下の画像中の ① ⇒ ② の順にクリックしてください。
そうすると、toio とのペアリングが行われ、うまくいくと toio から音が鳴ります。

toioのペアリング.jpg

また、ソースコードでログを表示する処理をいくつか入れていたため、ブラウザのコンソールを開くと例えば以下のような表示が確認できるかと思います(以下は、Google Chrome の開発者ツールを開き、コンソールを表示させた際の様子)。

開発者ツールのコンソール.jpg

あとは、MQTTブローカーも立ち上がった状態で、ペアリング済みの toio を開発用プレイマットにのせると、マット上の座標情報が MQTT で送信され、コンソールにログが表示されます。
今回、JavaScript で実装したソースコードで MQTT の送信側だけでなく受信側も実装したため、Unity側の準備ができていない状態でも、以下のようにログの中で MQTTブローカーを介した送受信の両方が行えていることが確認できます。

MQTTでPub・Sub_JavaScriptのみ.jpg

Unity関連の部分

地面・床・壁やプレイヤー等を準備する(コーディング以外の部分)

今回、Unity側の実装には公式チュートリアルを活用しました。
そして上で記載したとおり、地面・床・壁やプレイヤーの準備といった C# でのコーディング以外の部分は、以下を用いました。

●Unity入門チュートリアル 「玉転がし」(Roll-a-Ball) ビジュアルスクリプティング版 - Unity Learn
 https://learn.unity.com/project/bolt-roll-a-ball-tutorial

こちらは、プログラミングの部分をビジュアルスクリプティングの仕組みである BOLT を使う形になっているため、一部の内容は除きつつ進めて行きます。まずは、以下の内容を進めます。

上記の「Scene ビューの移動方法」の後に「Bolt のインストール」という項目があるのですが、今回は不要となる部分ですのでスキップしてください。
また、上記の「プレイヤーの追加」の中では、項目が 1 から 6 まで用意されていますが、「3.ゲームを再生する」の部分まで進めれば OK です(※ 4 から 6 は BOLT に関する内容となっているため)。

余談ですが、当初はこのチュートリアルで扱われていたビジュアルスクリプティングの「Bolt」も組み合わせようと思って、今回はスキップすると書いた部分も試作の過程では試したりしました。

C# による実装

これまでの手順で、Unity側のオブジェクトは準備ができました。
ここからは、MQTT を受信する処理と、受信した情報をもとに上記手順で作成したプレイヤーの位置を変える処理を実装します。

まず、MQTT関連の処理を行うために、MQTTクライアントライブラリを追加します。
ライブラリが複数ありそうだったのですが、以下の記事の記載を参考にしつつ MQTTnet を採用することにしました。

●MQTTnet を Unity で使う - Qiita
 https://qiita.com/johnson65t/items/230360b4cec41e8aafa4
●UnityからMQTTブローカに接続し、メッセージをUIとして表示させる - ゆべねこの足跡
 https://yubeshicat.hatenablog.com/entry/2019/02/10/033005

導入手順についても、上記の記事を参照しました。
手順としては、最初にライブラリを利用できるようにするために、NuGet Galleryから MQTTnet のパッケージをダウンロードしてきます。MQTTnet がいろいろありますが、上記の記事の両方で利用されていた 2.8.5 のパッケージを試すことにしました。
ダウンロードされたパッケージは展開をして、.NET 4.7.2 向けの DLL ファイル(mqttnet.2.8.5 > lib > net472 の中にあるもの)をプロジェクトにインポートします(MQTTnet.dll を、今回の Unityプロジェクトの Assets/Plugins/ 以下にインポートしてください)。
これで MQTTnet を利用するための準備ができました。

上記のライブラリの準備も完了したので、Unity側で C# のプログラムを書いていきます。
今回、MQTT で値を受け取ったら即座にオブジェクトの位置を変更する、という処理を実装したかったのですが、上記の 2つの記事にはオブジェクトの位置の変更に該当する部分がなさそうでした。そこで、他の記事を検索して探し、WebSocket を用いた事例ですが今回のオブジェクトの位置変更に活用できる内容が含まれた以下の記事を見つけることができました。

●WebブラウザとUnityをfirebaseで連携させたインタラクティブコンテンツ - KAYAC engineers' blog
 https://techblog.kayac.com/web-firebase-websocket-unity

そして、これらの記事などを参考にして実装したソースコードは以下となります。
処理の説明については、ソースコード中のコメントをご参照ください。

using UnityEngine;
using System;
using System.Text;
using System.Threading.Tasks;
using MQTTnet;
using MQTTnet.Client;

public class PlayerController : MonoBehaviour {
    private Vector3 position;

    // MQTT用の設定
    IMqttClient mqttClient;
    string url = "127.0.0.1";
    int port = 1883;  // MQTT over TCP

    async void Start () {
        // オブジェクトの位置操作用に準備
        position = transform.position;

        // MQTTクライアントの作成
        var factory = new MqttFactory();
        mqttClient = factory.CreateMqttClient();

        var options = new MqttClientOptionsBuilder()
            .WithClientId(Guid.NewGuid().ToString())
            .WithTcpServer(url, port)
            .Build();

        // MQTTブローカーとの通信の接続・切断時に行われる処理
        mqttClient.Connected += (s, e) => Debug.Log("接続したときの処理");
        mqttClient.Disconnected += async (s, e) => {
            Debug.Log("切断したときの処理");
            if (e.Exception == null) {
                Debug.Log("意図した切断です");
                return;
            }
            Debug.Log("意図しない切断です。5秒後に再接続を試みます");

            await Task.Delay(TimeSpan.FromSeconds(5));
            try {
                await mqttClient.ConnectAsync(options);
            }
            catch {
                Debug.Log("再接続に失敗しました");
            }
        };

        // MQTTブローカーからメッセージを受け取った時の処理
        mqttClient.ApplicationMessageReceived += (s, e) => {
            string payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload);
            Debug.Log("Payload = " + payload);

            // toioのx座標・y座標は「,」で連結して送っていたので、これを分離
            string[] xz = payload.Split(',');

            // toioの開発用プレイマットの座標を、Unity内の座標にマッピングする
            position = new Vector3(
                Map(float.Parse(xz[0]), 100, 400, -8, 8),
                0.5f,
                Map(float.Parse(xz[1]), 140, 355, 8, -8)
            );
        };

        // MQTTブローカーへの接続等
        await mqttClient.ConnectAsync(options);
        await mqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("test").Build());

        // Unity側からのPublishも後々のために試しに実装
        var message = new MqttApplicationMessageBuilder()
            .WithTopic("test")
            .WithPayload("Unity側からのメッセージ送信")
            .WithExactlyOnceQoS()
            .Build();
        await mqttClient.PublishAsync(message);
    }

    void Update() {
        // 更新された座標情報を適用する
        transform.position = position;
    }

    async void OnDestroy() {
        await mqttClient.DisconnectAsync();
    }

    // toioの開発用プレイマットの座標を、Unity内の座標にマッピングするための処理
    float Map(float value, float start1, float end1, float start2, float end2) {
        return start2 + (end2 - start2) * ((value - start1) / (end1 - start1));
    }
}

実行結果

上記の仕組みを実際に動かしてみた時の様子が、記事の冒頭にも掲載していた以下の動画です。

toio の動きに連動して、リアルタイムに Unity内のオブジェクトの位置が変わっているのがご覧いただけるかと思います。

今後の応用のために

まとめに入る前に、今回作った仕組みを今後発展させていくための方向性について、少し書いておこうと思います。

ゲーム的な要素

今回、実装しきれませんでしたが、構想としては以下のような内容を考えていました。

  • toio をコントローラーにしたゲーム
    • Unity側で敵キャラを出現させて弾よけゲーム的なものにする(+回収すべきアイテムを出現させたり)
      • 敵キャラとぶつかったら、Unity側から toio側へ MQTT で情報を送って、toio側でモーター制御等を使ったアクションを起こさせる
      • 敵キャラとぶつかったら、Unity側から M5Stack等の別デバイスに情報を送り、デバイス側で光(LEDテープを利用)・振動(モーター等を利用)など物理的なフィードバックをする演出を加える
  • toio を複数台使ったゲーム
    • toio 1台が操作用で残りがマット上を動くお邪魔キャラになり、それらをマット上で避けつつ Unity側で用意されたゲームをこなす
  • インターネットを介して通信するもの
    • MQTTブローカーをインターネット上のものにし、遠隔地の toio 同士で協力プレイ/対戦プレイをするような仕組みにする
    • MQTTブローカーをインターネット上のものにし、遠隔地のスマホ・PC上の操作をネット越しに送り、toio で遊ぶゲームのお邪魔キャラを操作して対戦する仕組みにする

ただ上記について、難易度が高くなりすぎそうだったり、操作性が悪くなりそうだったり、という方向のものがあるため(※ 特に、Unity の画面と開発用プレイマット側との両方を常に見ていないといけなくなりそうな部分)、仕組みは整理して検討しないといけない部分も多々ありそうだと考えています。

まとめ

今回、toio と Unity 内のオブジェクトの位置座標を、MQTT を用いた通信でリアルタイムに連動させる仕組みを実装してみました。

自分が Unity に慣れていないため、連動させる仕組みを試作した後の応用までは、今回手をつけることができませんでした。今後、上記の「今後の応用」で書いたような Unity側でのゲーム的要素等の追加実装や MQTT を活用した様々なデバイスとの連携の仕組みなど、追加していければと思っています。

また、Unity連携に限らず、toio を活用した様々な仕組みを作っていければと思います。

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5