PPP接続で広がるユースケース、UIFlow（MicroPython）とM5Stackによるセルラー通信

## はじめに
M5Stackシリーズのビジュアル開発環境であるUIFlowは、実行環境としてMicroPythonを利用しています。また、LTE-M接続用のユニットとして、SIM7080Gを搭載した[UNIT CatM+GNSS](https://docs.m5stack.com/en/unit/catm_gnss)なども販売されています。

一方で、現在のUIFlowのUNIT CatM+GNSS向けのブロックでは、通信モジュールに搭載されたプロトコルスタックを利用する形でしか通信ができません。このため使用できるHTTPメソッドに一部制約があったり、UIFlowにビルトインされているrequestsを使用するHTTPブロックやsocketを使用するTCP/UDPブロックを直接扱うことができず、Wi-Fiを前提としたプログラムをそのまま動作させることが困難です。
しかし、MicroPythonの`network.PPP`モジュールを利用すると、PPPoS接続でセルラー通信網に接続できるようになります。PPP接続に起因するオーバーヘッドなどのトレードオフはありますが、先述のような通信モジュール側のプロトコルスタック仕様に依存することで生じる制約を回避できます。

この記事では、UIFlow 2.0を利用してMicroPythonを書き込んだM5Stackからセルラー通信セッションを確立し、PPP接続をセットアップする手順を紹介します。
この方法を使用するとUIFlowにビルトインされた通信関連のブロック（すなわちMicroPython側にビルトインされたプロトコルスタック）をそのまま利用できるようになるため、Wi-Fiを前提としたプログラムのロジック部分も大きな変更を加えることなくセルラー通信経由で利用できるようになります。

:::note
`network.PPP` モジュール自体は以前からMicroPythonに実装されていましたが、私が2022年末頃に試した限りでは初代UIFlowやプレーンなESP32向けMicroPython v1.19.1をインストールしたM5Stackでは正常に動作しなかったため諦めていた方法です。
この記事で紹介する方法は、ここ1,2年のうちにMicroPythonに取り込まれた変更とUIFlow 2.0のオープンソース化でモジュールの構造や実装が確認できるようになったことで具体的に実現可能になったものです。
:::

## 準備するもの・前提条件
今回は以下の環境で検証しました。

### ハードウェア
- M5Stack Core2
- [UNIT CatM+GNSS](https://docs.m5stack.com/en/unit/catm_gnss)
- セルラー通信のためのSIMカード
    - 今回はSORACOM Air for セルラー（plan-D）のSIMを使用しました

### 実行環境
- [M5stack/uiflow-micropython](https://github.com/m5stack/uiflow-micropython) v2.0.5
- [UIFlow 2.0](https://uiflow2.m5stack.com/) (V2.0.5)

なお、以下の点はすでに完了しているものとし、本記事の説明対象外とします。
- uiflow-micropythonがM5Stackに書き込まれていること
- セルラー通信のために必要な設定が完了していること
- UIFlowの利用に必要な環境が整っていること

:::note warn
今回紹介する方法は最低限のセットアップ手順と使用方法の例のみです。必要に応じてエラーハンドリングや定期的に切断・再接続するような機構を追加してください。
また、筆者はこの機構を利用することによって生じるいかなる損害の責任も負いません。実装を参考にする場合は自身の責任において実施してください。
:::

## セットアップ
PPP接続関連のコードはおもにセットアップ時点で実行する必要がありますが、逆に言えばそれ以降のロジック部分はほとんど変更する必要がありません。

### インポート文の追加

下記のモジュールをインポートします（`requests` は必要に応じて追加してください）。`driver.simcom.*` はUIFlow 2.0.5以降で利用できるようです。
```python
import machine
import time
import network
import requests
from driver.simcom.sim7080 import SIM7080
from driver.simcom.common import AT_CMD
```
UIFlowの関数ブロックを利用する場合は下記のような形になります。
![スクリーンショット 2024-05-27 21.07.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182576/fb0f80f6-10bb-fb4f-f662-f9c8a9f9138b.png)


### 通信モジュールのセットアップ
通信モジュールのためのラッパークラスにUARTインスタンスを渡し、APN設定などを実施後、セルラー通信網にアタッチします（実行時に既存のセッションを明示的に切断しています）。
```python
def setup_modem():
  global MODEM, uart1, CELLULAR_STATUS
  print("Setting up MODEM...")

  MODEM = SIM7080(uart1)
  MODEM.modem_debug = True
  MODEM.set_gprs_network_state(0) # Detach network
  MODEM.set_pdp_context("soracom.io")
  MODEM.set_gprs_network_state(1) # Attach network

  CELLULAR_STATUS = MODEM.get_gprs_network_status() == 1
  if CELLULAR_STATUS == False:
    print("Failed to attach cellular network.")
    sys.exit(-1)
  print("Attached cellular network")
```

UIFlowの関数ブロックでは下記のような定義になります。
![スクリーンショット 2024-05-27 21.07.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182576/17ef2f9e-50ee-636a-967a-b3bc86dbfb1c.png)

:::note info
UIFlow V2.0.6 からUNIT CatM+GNSSのブロックが追加されたため、基本的なセットアップは定義済みのコードブロックで構成できるかもしれません。
:::

:::note warn
ここではUNIT CatM+GNSS（SIM7080G）を使用する想定でユーティリティ関数を利用してセットアップしています。キャリア選択や通信方式などより細かい設定項目もユーティリティ関数に定義があるようなので、必要に応じて指定してください。
https://github.com/m5stack/uiflow-micropython/tree/master/m5stack/libs/driver/simcom

なお、UNIT CatM+GNSS以外のユニットや通信モジュールを利用する場合は関数に定義のないコマンドを送信する必要があると思います。その場合は基底クラスにある [Modem#execute_at_command](https://github.com/m5stack/uiflow-micropython/blob/master/m5stack/libs/driver/simcom/common.py#L138)関数を利用するとよさそうです。
:::

### PPP接続の確立
通信モジュール側でダイヤルアップモードに遷移し、`network.PPP` モジュールをセットアップして接続を確立します。接続を確立すると、セルラー通信網を利用して通信できるようになります。
なお、disconnect_ppp_and_cellular関数はREPL（コマンドインタプリタ）から呼び出すことを想定しています。
```python
def setup_ppp():
  global MODEM, CELLULAR_STATUS, PPP, PPP_STATUS
  print("Setup PPP connection...")
  MODEM.execute_at_command(AT_CMD("ATD*99#", "CONNECT 150000000", 100000))

  PPP = network.PPP(uart1)
  PPP.active(True)
  PPP.connect(authmode=PPP.AUTH_CHAP, username="sora", password="sora")
  time.sleep_ms(3000)
  PPP_STATUS = PPP.isconnected()
  if PPP_STATUS == False:
    print("Failed to establish PPP")
    sys.exit(-1)

  _ipaddr = PPP.ifconfig()
  print(f"Established PPP connection, IP addr is {_ipaddr[0]}")

def disconnect_ppp_and_cellular():
  global MODEM, PPP
  print("Disconnecting...")
  PPP.active(False)
  time.sleep_ms(3000)
  MODEM.set_gprs_network_state(0)
```

UIFlowの関数ブロックでは下記のような定義になります。
![スクリーンショット 2024-05-27 21.08.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182576/d71f99dd-be5b-b60b-df14-531b5cefcb41.png)

## setup()関数の記述例
ここまでの関数ブロックを使って`setup()`関数を構成してみます。
```python
def setup():
  global MODEM, CELLULAR_STATUS, PPP, PPP_STATUS, uart1

  MODEM = None
  CELLULAR_STATUS = None
  PPP = None
  PPP_STATUS = None
  M5.begin()

  uart1 = UART(1, baudrate=115200, bits=8, parity=None, stop=1, tx=14, rx=13, txbuf=256, rxbuf=256, timeout=0, timeout_char=0, invert=0, flow=0)

  setup_modem()
  setup_ppp()
```

UIFlowの関数ブロックでは下記のような定義になります。
<img width="320" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182576/6e388c5f-d89b-3b53-8a45-aedbb51f9760.png">

## （例）HTTPリクエスト
あとはWi-Fiのときと同じようにビルトインされているHTTPブロックなどを使用してインターネット側のリソースにアクセスするだけです。UnitCatMの専用ブロックを使用する必要はありません。
下記ではインターネット側のリソースにアクセスする例として米Yahoo!FinanceのAPIから147A（ソラコム）の株価データを取得する例を示しています。
```python
def get_stock_info_147A():
  http_req = requests.get('https://query2.finance.yahoo.com/v8/finance/chart/147A.T', headers={})
  status_code = http_req.status_code
  print((str('Response stock info: status ') + str(status_code)))
  response_json = http_req.json()
  return response_json
```
![スクリーンショット 2024-05-27 21.17.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182576/676f7fcd-10ef-8ba8-b3e7-88253d002059.png)

# おわりに
セットアップは少々煩雑ですが、それ以外のロジック部分は基本的にはUIFlowのビジュアルプログラミングで実装を完結できるようになります。
また、ちょっとしたセットアップは必要ですがSORACOM Airを利用している場合はSORACOM Napter経由でリモートから[MicroPythonのWebREPLにアクセス](https://micropython-docs-ja.readthedocs.io/ja/latest/esp8266/tutorial/repl.html)してコマンド操作できるなど、UnitCatMモジュールの活用幅が飛躍的に広がります。ぜひお試しください！