Raspberry Pi pico W を使ってやる気が出るかもしれないスイッチを作る


## 何?
やらなきゃいけない事があるんだけど何かやる気が出ないなぁという時、本物のスイッチを操作すればちょっぴりやる気出たりしないかなーと思ったのでお試しで作ってみることにしました。

こういうものです。

![全体図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/355943/005efc1d-7c7e-55ae-209b-cc19e58ee43f.png)

こういったカバーの付いたトグルスイッチはミサイルスイッチと呼ばれるようです。
無意味にオン／オフしたくなる形をしていると思いませんか？
実際はカバーが邪魔だし力もいるので操作はしづらいのですが。
なんとなく爆弾っぽいなと思い7セグLEDでオンになってる時間を表示するようにしました。

そんなわけで、このおもちゃの機能は2つだけです。
- スイッチの状態をサーバ上に送信する
- スイッチがオンになっている時間を表示する

この記事はこのおもちゃを勉強がてら作ってみたーという小ネタです。
ソースコードは https://github.com/iuchim/yarukisw にあります。

あと投稿遅れましたが「株式会社愛宕 Advent Calendar 2023」の18日目の記事です。


## サーバ
Hono で作成し Cloudflare Workers上で動かします。
（今回はこれをとりあえず触ってみようというのが自分の裏の目的です👼）

サーバには次の2つの機能を実装します。
今回はPOSTしか使わないです。

- POST `/states/now`
  現在のスイッチの状態を保存
- GET `/states/:prefix`
  prefix を指定して状態を取得

またイタズラ防止目的で BASIC認証をかけてます。


### 前提
- node.js インストール済み
- Cloudflare アカウント作成済み


### プロジェクト作成
プロジェクト名は `yaruki-server` とします。

```shell
# テンプレートからプロジェクトを作成
$ npm create hono@latest yaruki-server
# template を聞かれるので、cloudflare-workers を選択

# yaruki-server と言うディレクトリが作成されているので移動しておく
$ cd yaruki-server
```

### Cloudflare へログイン
```shell
$ npm install wrangler --save-dev
$ npx wrangler login
# ブラウザが開くのでログインする
```


### Workers KV 作成
`state_kv` と言う名前で作成します。

```shell
$ npx wrangler kv:namespace create state_kv
```

binding と id が表示されてるので wrangler.toml を編集

```toml
name = "yaruki-server"
compatibility_date = "2023-01-01"

[[kv_namespaces]]
binding = "state_kv"
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```


### 認証用シークレット設定
BASIC認証で使用する情報を設定します。

ローカル用は `.dev.vars` ファイルに記述します。
```
username=test
password=test
```

本番環境用は `wrangler` コマンドで作成しておきます。
```shell
$ wrangler secret put username
# ユーザー名を入力

$ wrangler secret put password
# パスワードを入力
```


### コード作成

サンプルコードに毛が生えてる程度なので、全体を貼り付け。
最初 BASIC 認証部分を最後に書いてしまい、うまく動かず悩みました。


```typescript
import { Hono } from 'hono'
import { basicAuth } from 'hono/basic-auth'

// コンテキストの型
// 設定した情報はこの型に紐付けられる
type Bindings = {
  state_kv: KVNamespace,
  username: string,
  password: string,
}

const app = new Hono<{Bindings: Bindings}>()

// Basic 認証
app.use('*', async (c, next) => {
  const auth = basicAuth({
    username: c.env.username,
    password: c.env.password
  })
  return auth(c, next)
})

// prefix を指定して取得
app.get('/states/:prefix', async (c) => {
  const prefix = c.req.param('prefix')
  const list = await c.env.state_kv.list({ prefix })
  const results = await Promise.all(list.keys.map(async key => {
    const value = await c.env.state_kv.get(key.name)
    const timestamp = Number(key.name.replace(/^.*:/, ''))
    return { key: key.name, value, timestamp }
  }))
  return c.json({ ok: true, results })
})

// 現在時刻の状態を登録
app.post('/states/now', async (c) => {
  const param = await c.req.json()
  const value = String(param?.state ?? '')
  const now = new Date()
  const timestamp = now.getTime()
  const key = now.toLocaleString('sv-SE', { timeZone: 'Asia/Tokyo' })
    .replace(/[ \-:]/g, '') + ':' + timestamp
  await c.env.state_kv.put(key, value)
  return c.json({ ok: true, timestamp, key, value }, 201)
})

export default app
```


### ローカルで実行

```shell
# ローカルでサーバを起動
$ npm run dev
```

起動しておけば、コードを修正すると勝手にリロードしてくれます。

curl を使ってざっくり動作確認（本当はテスト書くべきですが…）

```shell
# ポート番号が 11111 だったとする
$ PORT=11111

# 登録
$ curl -X POST \
 -H 'Content-Type: application/json' \
 -d '{"state":"foo"}' \
 "http://test:test@localhost:${PORT}"

# 2023年12月分を取得
$ curl "http://test:test@localhost:${PORT}/states/202312"
```


ローカルで起動する場合、wrangler が miniflare を利用してローカルで同じような環境を構築し、その上でアプリを動かしてくれます。
この時実際のデータは `.wrangler/state/` 以下に保存されてます。
`wrangler` コマンドで確認したい場合は `--local` オプションをつけて実行すればOK。

```shell
$ npx wrangler kv:key list --binding=state_kv --local
```


### デプロイ

ローカルで確認が取れたらデプロイします。

```shell
$ npm run deploy
```

サーバ側は以上です。


## クライアント
とりあえず購入して積んでいた Raspberry Pi Pico W を利用します。

開発は MicroPython とし IDE には [Thonny](https://thonny.org/) を使ってみました。
（ほんとは TinyGo にしたかったんですが、どうもネットワーク周りが開発中っぽい？ので断念）


### パーツ
|名称|個数|補足|
|----|----|----|
|Raspberry Pi Pico W|1 |秋月電子で1,200円くらい |
|7セグLED モジュール（TM1637） |1 |Amazon で2個600円くらい |
|ミサイルスイッチ |1 |Amazon で3個1,000円くらい |
|ケース|1 |たまたま買ったイヤホンの箱を流用。ミサイルスイッチは力がいるので紙の箱は強度不足でダメダメです |
|その他配線|適宜|-|

配線は↓こんな感じです。

![配線.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/355943/9c2c3eaa-b4ff-29c4-a872-7d4c81bb88f8.png)

※実際はミサイルスイッチのほうの 3v3-⊕ 間には 330Ωの抵抗入れてます。
またよくフリーズした（後述）ので Pico W にリセットボタンも付けてます。


### 7セグLED モジュールの制御

7セグLED モジュールには TM1637 が使われています。
今回は [MicroPython TM1637](https://github.com/mcauser/micropython-tm1637) を利用して制御しています。

ただ動かしてしばらくするとフリーズするという現象に悩まされました。
配線間違えたりいろいろやらかしていたので Picc W 壊したかと思ったのですが、7セグLEDモジュールを使うのをやめると発生しなくなったので、通信がうまくできていないと判断しました。
ほんとは波形とか見て確認するべきですが、信号のタイミング遅くしたらいけるかなと思い [tm1637.py のソースコード](https://github.com/mcauser/micropython-tm1637/blob/cc9ecc642787b0c8afcb318a52865b855b4f0e5f/tm1637.py#L37) の TM1637_DELAY の値を 10us → 20us へ変更してみたところ改善したので今回はそのままいってます。

というか波形のなまりが原因だとするとプルアップ抵抗いれたらよかったのかな？
電子工作は雰囲気でやってるので勉強が必要です……。


### サーバとの通信

ハマりポイントその2、なかなか安定せず苦労しました。
送信できていてもしばらくすると `OSError: (-29312, 'MBEDTLS_ERR_SSL_CONN_EOF')` が出て送信に失敗してしまい、実はいまだにどう書けばいいのかよくわかっていません。

最終的に毎回 WiFi を切断して繋ぎ直せばエラーにならないようなので、今回はそうしてます。
そのため毎回5、6秒かかってしまうという…。
（マルチスレッド化も試していたのですが、うまく動作しなかったので今回は諦めてます）

現状の送信部分のコード（抜粋）

```python
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    wlan.connect(const.WIFI_SSID, const.WIFI_PASS)
    max_wait = 10
    while max_wait > 0:
        if wlan.status() < 0 or wlan.status() >= 3:
            break
        max_wait -= 1
        sleep_ms(1000)
    if wlan.status() != 3:
        raise RuntimeError('wlan status={}'.format(wlan.status()))
    url = const.API_URL + '/states/now'
    data = { 'state': 'on' if state else 'off' }
    auth = (const.API_USER, const.API_PASS)
    res = requests.post(url, json=data, auth=auth)
    res.close()
    wlan.disconnect()
```


## 感想
Hono + Cloudflare Workers はどちらも初めて触ったのですが、とても気軽に作れて楽しかったです。
今回はあまりにもお遊びな規模だったので、もう少し複雑になったときにどうなるかも試してみたいと思いました。

Raspberry Pi Pico W + MycroPython はとにかく苦労しました。
問題が発生した時に疑うことがたくさんあるのと、デバッグ環境が貧弱なのが辛いですね。
（デバッグ環境はもうちょっとなんとかできそうですが）
簡単なおもちゃを作るはずだったのに泥沼にハマって休日が溶けました。

おもちゃ自体について作りたてでまだあまり触れていないんですが、ミサイルスイッチを操作するのは妙な楽しさがあります。
ディスプレイつけて起動シーケンスっぽい演出入れたりするとテンション上がってやる気出るかもなーとは思っています。
ただミサイルスイッチって結構力がいる上に音が結構大きくてうるさいので、カバー付きボタンとかの方がいいかも。
今後は色々機能を足しつつ、最終的には会社の勤怠をつけられるようにしたいなーなどと考えています。