M5StickのボタンでLINE WORKSにスタンプする

LINE WORKS Advent Calendar 2023 の記事です。

https://qiita.com/advent-calendar/2023/lineworks

今年は物理なおもちゃと組み合わせて何か作ろうと思いたち、マイ道具箱に転がっていた M5StickC PLUS 連携してみました！

## M5StickC PLUS とは？

手のりサイズでコンパクトボディなIoTデバイスです。

WiFiやBluetoothを始め、液晶ディスプレイ、赤外線、ジャイロセンサー(IMU)、マイク、スピーカーも付いています。小型バッテリーを搭載していて、USB Type-C で充電して持ち運んで使えるのが特徴です。

更に拡張端子（GPIO）を使って、追加で拡張センサーやボタン、リレー、モーター等の駆動系など夢広がリングなおもちゃです。

M5Stack UIFlow という独自の制御ブロックを組み合わせて開発できるWebアプリがあり、プログラミングに馴染みが無くても始める事ができるのも特徴です。MicroPython や Arduino で開発することもできるので本格的に組み込むこともできます。

M5Stack シリーズとして展開されていて、色々なデバイスがスイッチサイエンスさん、マルツさん、秋月電子通商さんから販売されています。

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

https://www.switch-science.com/products/6470

調べてたら、後継品が出てる…。ほすぃ。

## 作ってみた

ボタンを押すと、色々なコニーがメリクリしてくれます。それだけですｗ

右下でカメラ撮影したデバイスのポチポチ押しが見れます。※押したときに音出ます。

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

スタンプはランダムな絵柄になるように組みましたが。
途中、コニーに何があったんでしょうね。ストーリー性が感じられました。

### 構成

M5StickC PLUS はかなりメモリ容量も少なく重たい処理は苦手なので MQTT プロトコルでボタンクリックのイベント通知を行う事にしました。

```mermaid
sequenceDiagram
  actor U as User
  participant B as M5Stick ボタン
  participant M as shiftr.io
  participant A as API Gateway + Lambda
  participant LW as LINE WORKS

  U->>B: クリック
  B-->>M: MQTT Publish
  M-->>A: Webhook POST
  A-->>LW: チャット送信
```

MQTT ブローカーに shiftr.io を利用して Webhook に変換してます。

その後は AWS を利用して API Gateway + Lambda の組み合わせで LINE WORKS との認証＋メッセージ送信を行っています。

## 実装紹介

### LINE WORKS のアプリ準備

LINE WORKS Developers のコンソールでAPIクライアントの登録とボットを作ります。

https://developers.worksmobile.com/jp/

#### API クライアント登録

スコープに `bot.message` を入れたAPIクライアントを用意しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/7d82df61-ee96-53f9-4ae0-ed9494d98982.png)

#### ボット作成

トークルームに参加できるよう、サンタコスのボットを作ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/fe7e6451-22c2-1084-8bee-908282ea835b.png)

### AWS: API Gateway + Lambda 部

LINE WORKS とのサーバー認証とトーク＆スタンプの送信を実装です。

今回はあらかじめ認証して得たリフレッシュトークンを元にしてアクセストークンを取得するようにしました。

<details><summary>通信部分 - lw-api.mjs</summary>

```javascript:lw-api.mjs
/* global fetch */

/** アクセストークンを取得(リフレッシュトークン経由) */
export async function refreshAccessToken({
  clientId,
  clientSecret,
  refreshToken,
}) {
  const body = new URLSearchParams();
  body.set("grant_type", "refresh_token");
  body.set("refresh_token", refreshToken);
  body.set("client_id", clientId);
  body.set("client_secret", clientSecret);

  const url = "https://auth.worksmobile.com/oauth2/v2.0/token";
  const response = await fetch(url, {
    method: "POST",
    headers: {
      "content-type": "application/x-www-form-urlencoded",
    },
    body,
  });

  if (!response.ok) {
    const msg = await response.json();
    console.error(msg);
    throw new Error(msg.error_description);
  }
  
  const payload = await response.json();
  return payload.access_token;
}

export function apiChannelMessage({ botId, channelId }) {
  return `https://www.worksapis.com/v1.0/bots/${botId}/channels/${channelId}/messages`;
}

/** チャンネルにメッセージ送信 */
export async function sendBotMessage({
  botId,
  channelId,
  content,
  accessToken,
}) {
  const url = apiChannelMessage({ botId, channelId });
  const response = await fetch(url, {
    method: "POST",
    headers: {
      authorization: `Bearer ${accessToken}`,
      "content-type": "application/json; charset=UTF-8",
    },
    body: JSON.stringify({ content }),
  });

  if (!response.ok) {
    const msg = await response.json();
    console.error(JSON.stringify(msg));
    throw new Error(msg.error_description);
  }
}
```

</details>

<details><summary>ハンドラー処理 - index.mjs</summary>

```javascript:index.mjs
import * as lwApi from "./lw-api.mjs";

const {
  LINEWORKS_CLIENT_ID,
  LINEWORKS_CLIENT_SECRET,
  LINEWORKS_REFRESH_TOKEN,
  LINEWORKS_BOT_ID,
  LINEWORKS_CHANNEL_ID,
} = process.env;

const random = (min, max) => min + Math.round(Math.random() * (max - min + 1));

export const handler = async (event) => {
  const body = JSON.parse(event.body);
  const talkMessage = body.message;

  // リフレッシュトークンで認証してアクセストークを得る
  const accessToken = await lwApi.refreshAccessToken({
    clientId: LINEWORKS_CLIENT_ID,
    clientSecret: LINEWORKS_CLIENT_SECRET,
    refreshToken: LINEWORKS_REFRESH_TOKEN,
  });

  // Webhookで受け取ったメッセージをトークとして送信
  await lwApi.sendBotMessage({
    botId: LINEWORKS_BOT_ID,
    channelId: LINEWORKS_CHANNEL_ID,
    accessToken,
    content: {
  		type: "text",
  		text: talkMessage,
    },
  });

  // パッケージ538のID範囲でスタンプを送信
  const stickerId = random(2612, 2651);
  await lwApi.sendBotMessage({
    botId: LINEWORKS_BOT_ID,
    channelId: LINEWORKS_CHANNEL_ID,
    accessToken,
    content: {
  		type: "sticker",
  		packageId: "538",
  		stickerId: `${stickerId}`
    },
  });

  return {
    statusCode: 201,
    body: 'OK',
  };
};
```

</details>

![screencapture-ap-northeast-1-console-aws-amazon-lambda-home-2023-12-24-22_55_26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/40470221-bfb7-1cdc-b7ba-7e9467bf67f7.png)

`index.mjs` で受け取る諸々の値を環境変数に設定しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/d3c79a44-cd15-32b4-dd70-ac760d6d438e.png)

チャンネルIDは決め打ちで作ったので、ボットと参加しているトークルームのメニュー「チャンネルID」から取得して使います。

#### リフレッシュトークンの生成方法

Developers ドキュメント「Service Account 認証 (JWT)」を参照して、アサーションを作成しつつトークンエンドポイントに POST して得られます。

https://developers.worksmobile.com/jp/docs/auth-jwt

#### API Gateway

shiftr.io の Webhook イベント送信先として受け取れるよう、API Gateway にルート（パス）を作成して、POSTメソッドとして作成したLamdaを統合します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/a74a8b1c-9de1-9492-a862-81177f5faf5f.png)

これですべての連携ができました。

### shiftr.io 部

小規模であれば無料で使える MQTT ブローカーです。
Webhook ターゲットをトピックのサブスクライブとして設定できるのですごく便利でした。

https://www.shiftr.io/

#### インスタンス起動

アカウント作ってサインインすると、インスタンスを作れるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/3c931faa-8c6d-c31c-d1d0-3db9de63b9f1.png)

無料の Basic プランを選んで、インスタンス名を設定したらあとは起動するだけ。
※プラン説明にもありますが１日に起動できるのは最大で 6 時間です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/f87861e6-6acd-ad16-6e90-6c34f4c65064.png)

#### トークンの発行

MQTT トークンを発行して、M5SickC PLUS アプリの Setup ブロック内に仕込みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/6d6ee555-1792-4444-3182-fb764ccb06a2.png)

#### Webhook をターゲットとしたサブスクライブ

トピック `advent/say` で受け取ったメッセージを、AWS の API Gateway で用意したエンドポイントに POST 送信するようにします。この時に単純なテキスト文字列を JSON 風に組み替えています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/7bda59cb-c711-4cae-da95-206a49a0731c.png)


### M5StickC PLUS アプリ部

UIFlow 2.0 で作りました。

![screencapture-uiflow2-m5stack-2023-12-24-19_41_00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/e56a7dc0-dfdb-520f-e202-4d4510a628b2.png)

左側の `Setup` ブロック部で起動時の初期化＋MQTT接続をしています。

起動直後からWiFiネットワークの接続完了までラグがあるので、接続完了までループして監視してから接続しています。

メインとなるボタンのクリック処理は、中央上部にある `When button BtnA was Clicked` ブロック部でしています。クリックのフィードバックとしてスピーカーからビープ音を出すようにしてます。

なお、MQTT で送信するメッセージは「**メリークリスマス!!**」固定です。

MQTT の送信先は shiftr.io で用意したサーバー、トークンのユーザー名・パスワードを指定します。

## おまけ

開発小話です。

### MQTTBox

MQTT を始めて触りました。それで設定が正しいのか送受信できているのすら分からなかったところで MQTTBox なるものの存在を知りました。これでデバッグがかなり楽になりました。

https://apps.microsoft.com/detail/9NBLGGH55JZG

### AWS IoT

AWS を使うなら、AWS IoT があるじゃない？わざわざ別のサービスを挟まなくても。
と最初は思い、それでやろうとしたら見事にハマりました。AWS IoT だとTLS必須で、クライアント証明書の設定がいるのですが、何故か UIFlow 2.0 だと所定の手順で証明書・秘密鍵ファイルの指定が出来きず終いで今回は諦めました。
別の機会にリベンジしたい。

それと同時に、ルール設定なり本題とは離れたことも多く必要になりそうだったので、これも別途チャレンジです。

## さいごに

物理モノを扱うために開発と準備をしていたのですが、久しぶりにM5Stackに触れて楽しすぎて本来の目的からそれてついうっかり日が暮れてしまいました。
今年は、童心に返ったようにおもちゃで遊ぶ懐かしみも体験できた LINE WORKS アドベントになりました。

メリークリスマス！