Raspberry Pi Pico WでWebMiteを使う〜Webサーバ

# はじめに

WebMiteを使うことでTCP/IPを利用できるようになります。[PicoMite User Manual](https://geoffg.net/Downloads/picomite/PicoMite_User_Manual.pdf)に記載されているWebサーバに関する記述をもとにWebMiteのWebサーバ機能を使ってみた。

# TCPサーバの起動と停止

## TCPサーバの起動

WebMiteでWebサーバを起動するには`OPTION TCP SERVER PORT nn`コマンドを使ってTCPのサーバを構成します。nnはサーバのポート番号です。Webサーバ、httpのポート番号は一般的に80なので次のようにコマンドプロンプトで実行します。コマンド実行後にWebMiteが再起動します。このコマンドは一度実行すれば保持されます。

```
> OPTION TCP SERVER PORT 80
PICO0123456789A connecting to WiFi...
Connected 192.168.10.24
Starting TCP server at 192.168.10.20 on port 80
```

試しにWebブラウザで`OPTION`コマンドの出力したIPアドレスにアクセスしてもアクセスに対する応答のプログラムを実行していないので何も表示されない。Wiresharkで確認すると応答コード404を受信していました。

## TCPサーバの停止

起動しているTCPサーバーを停止するときには起動時に使ったコマンドのポート番号を0にして実行します。実行後WebMiteは再起動します。

```
> OPTION TCP SERVER 0
```

## サーバの最大接続数

TCPサーバが応答できる最大接続数は`MM.INFO(MAX CONNECTIONS)`で確認できます。以下が実行結果で、最大8接続できることを示しています。

```
> PRINT MM.INFO(MAX CONNECTIONS)
 8
```

# Webサーバ機能の実装

`OPTION TCP SERVER PORT 80`コマンドで80ポートを開きますがクライアントからの要求に応答するWebサーバーのプログラムはないため作成します。
80ポートへのアクセスが発生した場合に実行するサブルーチンは次のコマンドで指定します。これは割り込み処理になります。パラメータ`interruptSub`に実行するサブルーチン名を指定します。

```
WEB TCP INTERRUPT interruptSub
```

## サーバプログラム

[PicoMite User Manual](https://geoffg.net/Downloads/picomite/PicoMite_User_Manual.pdf)に掲載されたプログラムをもとにADT7410温度センサーで計測した温度をOLEDディスプレイに表示し、Webサーバーへアクセスすると温度を表示するプログラムを作成した。

Webサーバのプログラムは次の処理を実行します。

1. クライアントから要求文字列を受信バッファに格納する
2. 適切なHTTPヘッダかを確認する
3. 適切なHTTPヘッダならばクライアントが要求しているリソースが存在するかを確認する
4. 要求されたリソースが存在する場合はそれをクライアントに返信する

### 変数宣言など

変数宣言部を以下に示します。行番号は全体の通し番号です。
3行目に80番ポートへされたときに実行するサブルーチン`WebInterrupt`を指定しています。
5〜9行目は温度センサーADT7410の温度データを格納する変数やI2C接続のピン番号を指定しています。

```BASIC
     2	Option Explicit                ' 変数宣言を強制する
     3	WEB TCP INTERRUPT WebInterrupt ' Webアクセス時の処理サブルーチン指定
     4	' 変数宣言
     5	Dim data%(1), raw%, temp!      ' ADT7410空データ保存、温度データ用
     6	Dim stemp$,datestr$            ' 表示用文字列保存用
     7	Const TMP_ADDR = &H48          ' ADT7410のI2Cアドレス
     8	SetPin GP16, GP17, I2C　　      ' OLED I2C接続ピン
     9	I2C OPEN 100, 100　　　　　      ' I2Cチャンネル1を開く
```

### ADT7410温度センサーのデータ処理

[Raspberry Pi PicoでPicoMite(MMBasic)を使う〜OLEDディスプレイへの出力](https://qiita.com/ksakabe/items/48204558f388ad1daede)で作成したコードを流用している。

下記のコードを無限ループで実行している。

- 12、13行目：日時文字列の作成
  ブラウザ上に表示する日時の文字列を作って変数`datestr$`に格納しています。MMBasicで取得できる日付の形式がYYYY/MM/DDでないため、YYYY/MM/DD形式に変換しています。
- 14〜18行目：温度の取得
  で温度センサADT7410からデータを読み取り、温度へ変換しています。
- 19行目：日時文字列をOLEDディスプレイに出力
- 20行目：温度を整数部2桁、小数部1桁の文字列に変換
- 21行目：温度文字列をOLEDディスプレイに出力

```
    11	Do
    12	  datestr$ = Mid$(Date$, 7, 4) + "/" + Mid$(Date$, 4, 2) + "/" + Mid$(Date$, 1, 2)
    13	  datestr$ = datestr$ + " " + Time$
    14	  I2C READ TMP_ADDR, 0, 2, data%()
    15	  raw% = data%(0) << 8 Or data%(1)
    16	  raw% = raw% >> 3
    17	  If raw% >= 4096 Then raw% = raw% - 8192
    18	  temp! = raw% / 16.0
    19	  Text 0, 0, Time$,,3
    20	  stemp$ = Str$(temp!, 2, 1)
    21	  Text 0, 32, stemp$ + Chr$(&H60) + "C",,3
    22	  Pause 5000
    23	Loop
 
```

### Webアクセス時の処理ルーチン

25〜50行が80番ポートへのアクセス時に処理されるサブルーチンです。 [PicoMite User Manual](https://geoffg.net/Downloads/picomite/PicoMite_User_Manual.pdf)の75ページに記されているコードをそのまま使っています。

処理内容は下記のとおりです。

- 26行目：内部処理に必要なローカル変数、buff%()はクライアントから受信したメッセージを格納するバッファ
- 27〜49行目：複数接続に対応するためのループ
- 28行目：クライアントからのメッセージを読み取り、バッファに格納
- 29行目：バッファから文字列GETの位置を取得。`LINSTR`は長い文字列から指定された文字列の位置を返す関数です
- 30行目：バッファから文字列HTTPの位置を取得。
- 31行目：HTTPのGETメソッドによるリソースの取得かの判定
  HTTPでWebサーバーのリソースを取得するときはHTTPヘッダに`GET / HTTP/1.1`という文字列が含まれるなずなので文字列GETがあり（p1% > 0）、かつHTTPがあり（p2% > 0）かつGET、HTTPの順（P2% > p1%）であるかを判定している。
- 32行目：クライアントが取得しようとしているファイル名を取得し、小文字に変換して変数`file$`に格納
  `LGetStr$`関数で長い文字列（buf%()）のp1% + 4番目からp2% - p1% - 5文字分を取り出し、`LCASE$`関数で小文字に変換している
  GETメソッドが`GET /Hello.html HTTP/1.1`の場合、`/hello.html`が得られる
- 33行目：ファイル名が`/`だけの場合は`/index.html`に変更する
- 34行目：35行目がエラーでも無視する
  `ON ERROR SKIP`は次の行のコマンドでエラーがあってもそれを無視します。読み取り専用変数`MM.ERRNO`に0以外の値がセットされます
- 35行目：変数file$のファイルを読み取りモードで開く。存在しない場合はエラーになるが、直前の`ON ERROR SKIP`でプログラムの実行は継続する
- 36行目：エラーがあった場合（`MM.ERRNO`が0以外）は応答コード404を返す
- 37行目：35行目で開いたファイルを閉じる
- 38〜47行目：ファイルの拡張子（ファイル名の末尾4文字`RGIHT$(file$, 4)`）により処理を分ける
	- HTMLやテキストファイル：ファイルの内容をクライアントに返信
	- 画像（BMP、PNG、JPEG、ICO）：Content-Typeを設定してファイル内容をクライアントに返信

### すべてのコード

以下が完全なコードです。

```BASIC:WebSvr.bas
     1	CLS
     2	Option Explicit                ' 変数宣言を強制する
     3	WEB TCP INTERRUPT WebInterrupt ' Webアクセス時の処理サブルーチン指定
     4	' 変数宣言
     5	Dim data%(1), raw%, temp!      ' ADT7410空データ保存、温度データ用
     6	Dim stemp$,datestr$            ' 表示用文字列保存用
     7	Const TMP_ADDR = &H48          ' ADT7410のI2Cアドレス
     8	SetPin GP16, GP17, I2C　　      ' OLED I2C接続ピン
     9	I2C OPEN 100, 100　　　　　      ' I2Cチャンネル1を開く
    10
    11	Do
    12	  datestr$ = Mid$(Date$, 7, 4) + "/" + Mid$(Date$, 4, 2) + "/" + Mid$(Date$, 1, 2)
    13	  datestr$ = datestr$ + " " + Time$
    14	  I2C READ TMP_ADDR, 0, 2, data%()
    15	  raw% = data%(0) << 8 Or data%(1)
    16	  raw% = raw% >> 3
    17	  If raw% >= 4096 Then raw% = raw% - 8192
    18	  temp! = raw% / 16.0
    19	  Text 0, 0, Time$,,3
    20	  stemp$ = Str$(temp!, 2, 1)
    21	  Text 0, 32, stemp$ + Chr$(&H60) + "C",,3
    22	  Pause 5000
    23	Loop
    24
    25	SUB WebInterrupt
    26	  LOCAL a%, p1%, p2%, file$, buff%(4096)
    27	  FOR a% = 1 To MM.INFO(MAX CONNECTIONS)
    28	    WEB TCP READ a%, buff%()
    29	    P1% = LINSTR(buff%(),"GET")
    30	    P2% = LINSTR(buff%(),"HTTP")
    31	    If (p1% <> 0) And (p2% <> 0) And (p2% > p1%) Then
    32	      file$ = LCASE$(LGetStr$(buff%(), p1% + 4, p2% - p1% - 5))
    33	      IF file$ = "/" THEN file$ = "/index.html"
    34	      ON ERROR SKIP
    35	      OPEN file$ FOR INPUT AS #1 ' ファイルの存在をチェック
    36	      IF MM.ERRNO THEN WEB TRANSMIT CODE a%, 404 : CONTINUE FOR
    37	      CLOSE #1
    38	      SELECT CASE RIGHT$(file$, 4)
    39	        CASE "html", ".htm", ".txt"
    40	          WEB TRANSMIT PAGE a%, file$
    41	        CASE ".bmp", ".png", ".gif"
    42	          WEB TRANSMIT FILE a%, file$, "image/" + RIGHT$(file$, 3)
    43	        CASE ".jpg", "jpeg"
    44	          WEB TRANSMIT FILE a%, file$, "image/jpeg"
    45	        CASE ".ico"
    46	          WEB TRANSMIT FILE a%, file$, "image/vnd.microsoft.icon"
    47	      END SELECT
    48	    ENDIF
    49	  NEXT a%
    50	END SUB
    51
    52	I2C CLOSE
    53
    54	End
```

## HTMLファイル

サーバからクライアントへ転送するHTMLファイルにはサーバのプログラム（上記WebSvr.bas）で処理した結果、ここでは日時（`datestr$`）と温度（`sTemp$`）の2つの変数値をHTMLに含めています。
HTMLコード中に変数を含める場合は`{`と`}`で囲みます。下記のindex.htmlの10行目の`{datestr$}`と`{stenp$}`です。

```HTML:index.html
     1	<!DOCTYPE html>
     2	<html lang="ja">
     3	<head>
     4	<meta charset="UTF-8">
     5	<title>WebMite Test</title>
     6	</head>
     7	<body>
     8	<h1>WebMite test</h1>
     9	<h2>温度モニタ</h2>
    10	<p>{datestr$}の温度は{stemp$}℃です</p>
    11	</body>
    12	</html>
```

# 実行結果

MMBasicのプログラム、HTMLファイルをアクティブドライブに配置し、MMBasicのプログラムを実行します。同一ネットワーク内のMacなどのブラウザを開き、Raspberry Pi Picoに割り当てられたIPアドレスでアクセスします。この例の場合はhttp://192.168.10.24/ になります。

<!-- ここにブラウザの画像 -->
![スクリーンショット 2025-05-30 20.19.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72921/4da2c264-0e81-4729-9fc0-5a0fe51d2293.png)

WebMiteでの処理に時間がかかり、タイムアウトや応答しなくなることがあり、プログラムが停止することがあります。

```
[43] WEB TRANSMIT PAGE a%, file$
Error : No response to request from connection no. 7
> Warning: LWIP send data timeout
Error : No response to request from connection no. 1
>
Error : No response to request from connection no. 2
>
Error : No response to request from connection no. 3
> print MM.ERRNO
 16
>
```

ユーザーズマニュアルには「インターネットプロトコルの実装にはプロセッサのリソース（特にRAM）が大量に使用されるため、WebMiteファームウェアは、WiFiとインターネット接続が重要で、標準のRaspberry Pi Picoと比較してパフォーマンスに多少の影響があっても許容できる場合にのみ使用してください。」とあり、特にRaspberry Pi Pico Wを使用する場合には注意が必要です。Raspberry Pi Pico 2 Wは Pico Wよりもメモリ容量が大きいので上記のようなエラーメッセージは試した限りでは表示されなかった。

# 参考サイト

- [PicoMite User Manual](https://geoffg.net/Downloads/picomite/PicoMite_User_Manual.pdf)