旅する医用工学者

医用工学に関するトピックを中心に,臨床工学技士国家試験,第1種ME技術実力検定試験,第2種ME技術実力検定試験に関係する内容についても取り扱います.日々の技術開発や受験勉強や学校の授業の予習・復習にお役立てください.内容の正確性には留意していますが,これを保証するものではありません.

M5Stamp C3U + MicroPython による気象庁天気予報データのスクレイピング

M5Stamp C3Uで遊んでみよう!

前回の記事でWindow + MicropythonでM5Stamp C3Uを開発する環境構築について紹介した。

medicalengineer.hatenablog.jpしかし、Lチカをしただけでは芸がない。何か役に立つモノを作ってみたくなるのが人情ではないだろうか。M5Stamp C3UはWiFi接続も可能なデバイスであるため、Webから何かのデータを持ってきて表示するのも面白そうだ。よし、早速天気予報でもスクレイピングしてみよう。

 

天気予報のスクレイピング

Web上で天気予報を確認する方も多いだろうが、国内の天気予報サイトの代表格といえば気象庁のサイトではなかろうか。

気象庁による東京地方の天気予報のページ

気象庁の天気予報ページは、jsonとよばれるデータ記述言語で記載されたデータを参照して自動的に生成されている。

東京都の天気予報を表示するためのjson

 

なお、このjsonデータの読み方については、気象庁防災情報XMLフォーマット情報提供ページに詳細が掲載されている。

xml.kishou.go.jp気象庁の天気予報ページが参照しているjsonファイルの中で用いられている"areas"や"weatherCodes"等の項目の対応に関しては、"電文毎の解説資料(令和4年7月28日一部更新)"に収録されている"府県天気予報・府県週間天気予報_解説資料付録.xls"に掲載されている。

https://xml.kishou.go.jp/jmaxml_20220728_Manual(pdf).zip

今回は、この資料を参考に、気象庁jsonのデータを読み取って、M5Stamp C3Uに接続した小型ディスプレイに表示してみよう。

 

スクレイピングの準備

スクレイピングの準備として、予報対象地域のコードを確認しよう。例えば、東京都の天気予報のページのURLは、

"https://www.jma.go.jp/bosai/#pattern=forecast&area_type=offices&area_code=130000"

であるが、"area_code"の後に続く6桁の数字が各都道府県に対応している。すなわち、東京都のコードは130000だ。ただし、エリアの広い北海道、鹿児島、沖縄の各道県は道県単位ではなく、地方単位(例えば、北海道ならば宗谷、上川・留萌、網走・北見・紋別……)での対応となる。上記ページから都道府県・地方を選択するメニュー画面を出し、対象の都道府県(以上3道県は地方)の天気予報を表示する。

都道府県・地方の選択メニュー

 

対称の都道府県の天気予報を表示したら、そのURLから"area_code"を確認する。例えば、北海道宗谷地方のURLは

"https://www.jma.go.jp/bosai/#pattern=forecast&area_type=offices&area_code=011000"

であるので、コードは011000だということがわかる。これでスクレイピングの準備は完了だ。

 

パッケージのインストール

続いてThonnyを用いて必要なパッケージをインストールする。今回作成するプログラムでは、SSD1306を制御するための"micropython-ssd1306"と、Webデータを取得するための"micropython-urequest"という2つのパッケージを利用するので、それらをインストールしよう。

Thonny上で"Tools"→"Manage packages"とクリックすると、マイコンにインストールされているパッケージの管理画面が表示される。検索ウィンドウに"micropython-ssd-1306"と入力して、"Search on PyPI"をクリックして検索する。

mycropython-ssd1306ライブラリのインストール

"micropython-ssd1306"を選択し、"Install"をクリックしてインストールする。

mycropython-ssd1306ライブラリのインストール

同様に"micropython-urequest"も検索してインストールする。2つのパッケージのインストールが完了したら、"Close"をクリックしてウィンドウを閉じておく。

 

配線

さて、早速回路を組み立ててゆこう。今回の作例であれば、M5Stamp C3Uのほか、128×32pixelのOLEDディスプレイ、ジャンパ線4本(接続を工夫すれば減らすことも可能)、ブレッドボードが必要だ。

配線は極めて単純で、M5Stamp C3UにSSD1306 OLEDディスプレイをI2C接続するだけだ。OLEDのVCCはM5Stamp C3Uの任意の5Vピン、GNDは任意のGNDピンに接続すればよく、SCLとSDAはプログラム上で指定すればよい。今回は、SCLは4番ピン、SDAは3番ピンを使うこととした。

配線

コーディング

配線が済んだらコーディングだ。Thonnyで以下のコードを入力(もしくはCopy & Paste)する。ただし、"your_ssid"はWiFiSSID、"your_password"はWiFiのパスワードを入力し、"https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json"の"130000"部分は対象の都道府県(北海道、鹿児島、沖縄の3道県は地方)のコードを入力する。さらに、都道府県内の地域の選択は"areas"の引数(0, 1, 2,...)を変更してやればよい。

# -*- coding:utf-8 -*-

import machine
from machine import Pin, SoftI2C
import ssd1306
import urequests as requests
import json
import network

ssid = "your_ssid"
password = "your_password"

wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
    print('connecting to network...')
    wlan.connect(ssid, password)
    while not wlan.isconnected():
        pass
print("WiFi Connected")

i2c = SoftI2C(scl = Pin(4), sda = Pin(3))
display = ssd1306.SSD1306_I2C(128,32,i2c)

def decodeWeather(weatherCodes):
    if weatherCodes in [100,123,124,130,131]:
        weather = "Sunny"
    elif weatherCodes in [101,132]:
        weather = "Sunny / Cloudy"
    elif weatherCodes in [102,103,106,107,108,120,121,140]:
        weather = "Sunny / Rainy"
    elif weatherCodes in [104,105,160,170]:
        weather = "Sunny / Snowy"
    elif weatherCodes in [110,111]:
        weather = "Sunny > Cloudy"
    elif weatherCodes in [112,113,114,118,119,122,125,126,127,128]:
        weather = "Sunny > Rainy"
    elif weatherCodes in [115,116,117,181]:
        weather = "Sunny > Snowy"
    elif weatherCodes in [200,203,206,207,208,220,221,240]:
        weather = "Cloudy"
    elif weatherCodes in [201,223]:
        weather = "Cloudy / Sunny"
    elif weatherCodes in [202,203,206,207,208,220,221,240]:
        weather = "Cloudy / Rainy"
    elif weatherCodes in [204,205,250,260,270]:
        weather = "Cloudy / Snowy"
    elif weatherCodes in [210,211]:
        weather = "Cloudy > Sunny"
    elif weatherCodes in [212,213,214,218,219,222,224,225,226]:
        weather = "Cloudy > Rainy"
    elif weatherCodes in [215,216,217,228,229,230,281]:
        weather = "Cloudy > Snowy"
    elif weatherCodes in [300,304,306,328,329,350]:
        weather = "Rainy"
    elif weatherCodes in [301]:
        weather = "Rainy / Sunny"
    elif weatherCodes in [302]:
        weather = "Rainy / Cloudy"
    elif weatherCodes in [303,309,322]:
        weather = "Rainy / Snowy"
    elif weatherCodes in [308]:
        weather = "Rainstorm"
    elif weatherCodes in [311,316,320,323,324,325]:
        weather = "Rainy > Sunny"
    elif weatherCodes in [313,317,321]:
        weather = "Rainy > Cloudy"
    elif weatherCodes in [314,315,326,327]:
        weather = "Rainy > Snowy"
    elif weatherCodes in [340,400,405,425,426,427,450]:
        weather = "Snowy"
    elif weatherCodes in [401]:
        weather = "Snowy / Sunny"
    elif weatherCodes in [402]:
        weather = "Snowy / Cloudy"
    elif weatherCodes in [403,409]:
        weather = "Snowy / Rainy"
    elif weatherCodes in [406,409]:
        weather = "Snowstorm"
    elif weatherCodes in [361,411,420]:
        weather = "Snowy > Sunny"
    elif weatherCodes in [371,413,421]:
        weather = "Snowy > Cloudy"
    elif weatherCodes in [414,422,423]:
        weather = "Snowy > Rainy"
    else:
        weather = "Unknown"
    return weather

jma_url = "https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json"
jma_json = requests.get(jma_url).json()
jma_date = jma_json[0]["timeSeries"][0]["timeDefines"][0]
jma_weather = jma_json[0]["timeSeries"][0]["areas"][0]["weathers"][0]
jma_weatherCodes = int(jma_json[0]["timeSeries"][0]["areas"][0]["weatherCodes"][0])

today_date = jma_date[0:10]
today_date = today_date.replace("-","/")
jma_weather = jma_weather.replace(" ", "")
today_weather = decodeWeather(jma_weatherCodes)

print(jma_date)
print(jma_weather)
print(jma_weatherCodes)

display.text(today_date,0,0,1)
display.text(today_weather,0,16,1)
display.show()

Thonnyによるコーディング

コーディングが済んだら、"File"→"Save as"とクリックし、"MicroPython device"を選択し、"main.py"というファイル名を指定して保存する。

保存した後、F5キーを押すか、"Run current script"をクリックすれば書き込んだプログラムが実行され、ディスプレイには日付と天気予報が表示されている筈だ。

 

動作確認


また、ThonnyのShellには上記コードのprint文の内容が表示されている筈だ。スタンドアロンで動作させるときはShellの内容は確認できないのだが、万一うまく動作しないようであれば、このShellの内容をもとにデバッグすることとなるため、要所要所でShellにメッセージを出力させるような設計にしておくとよい。

print命令の内容はShellに表示される

以上、M5StampとMycroPythonを用いたWebスクレイピングを試してみた。慣れれば半日かからずに完成できるため、小学生から高校生の皆さんの夏休みの自由研究にもちょうどいいかもしれない。

 

Windows + MicroPythonによるM5Stamp C3U開発環境の構築

Windows + MicroPythonによるM5Stamp C3U開発環境の構築

M5Stamp C3Uとは?

M5Stamp C3UはESP32-C3 RISC-V MCUを搭載した切手サイズのマイコンボードであり、ESP32-C3のUSBシリアル変換機能を利用しているため、直接USB接続するだけでプログラムの書き込みが可能なデバイスだ。

www.switch-science.comしかし、2022年8月現在、M5シリーズの開発環境であるUIFlowに対応していないことから、何かと不便なことが多い。Pythonで開発したいユーザはM5 Atom Liteを購入してUIFlowのPythonエディタを利用するのも一つの選択肢なのだが、残念ながらUI FlowのPythonエディタはPythonのコードを保存することができない。

そこで、UIFlowに頼らずにPythonでの開発環境を整えることとしたのだが、Windows環境では何かと面倒なことが多かった。個人的な備忘も兼ねて、要点をまとめておくことにする。

 

Pythonのインストール

もし、WindowsPythonがインストールされていない場合、Pythonをインストールする。Python本体のあるフォルダにPathを通しておくとともに、必要に応じて適切な環境を構築する。

www.python.org

もし、わかりにくければPython.jpの情報を参考にしてもよい。

www.python.jp

実は、Unix系OSであればさほど大変ではないのだが、WindowsPython環境を構築するのは結構面倒な作業だったりする。うまくいかなければ、Windowsなんて諦めてUnix系OSを導入してしまうのも一つの作戦なのだが、そうするとこの記事の価値がなくなるので、Windows環境上でゴリ押ししてもらいたい。

 

CP201x ドライバのインストール

Pythonの環境が整ったら、USB経由でファームウェアやプログラムを書き込むためのチップ(CP210xシリーズ)のドライバをインストールする。

www.silabs.comSILICON LABSのCP210xドライバのページから"DOWNLOADS"タブへ入り、"Software Downloads"の項目から"CP210x Windows Drivers"を選択し、ダウンロードする。環境によっては、単にリンクをクリックするだけではダウンロードできないこともあるようだ。その場合は、リンクを右クリックしてリンク先を保存するようにすればよい。

CP210x Driverのダウンロード

ZIPファイルを解凍したら、64bit版のWindowsであれば"CP210x VCPInstaller_x64.exe"を、32bit版であれば"CP210x VCPInstaller_x86.exe"をダブルクリックしてドライバをインストールする。

CP210x Driverのインストール

 

ESPTOOLのダウンロード

次はESP32C3のファームウェアの書き込みに必要なツール(ESPTOOL)のダウンロードだ。ESPTOOLはGitHubからダウンロードできる。

github.com上記リンクにアクセスしたら、"Code"メニューをクリックし、"Download ZIP"を選択する。

ESPTOOLをGitHubよりダウンロード

ダウンロードしたZIPファイルを解凍し、その中にある"esptool.py"をPathの通ったフォルダに格納(コピーまたは移動)する。

ダウンロードしたESPTOOL

 

ESP32C3ファームウェアのダウンロード

MycroPython公式サイトより、ESP32C3用のファームウェアをダウンロードする。

micropython.orgいくつかのバージョンが選択できるようになっているが、特段の事情がなければ、Releasesの中から、最新のもの(latest)を選んでおけばよい。

ESP32ファームウェアのダウンロード

ダウンロードしたら、わかりやすいフォルダにでも移動しておこう。これで必要なファイルの準備は完了だ。

 

ESP32C3ファームウェアの書き込み

必要なファイルが揃ったら、いよいよM5Stamp C3Uにファームウェアを書き込むことになる。M5Stamp C3Uの中央部のボタンを押しながら、USBケーブルでPCに接続する。これでM5Stamp C3Uはファームウェアを書き込むモードに入る。

Raspberry Pi  PicoをMicroPython環境で開発した経験のある方ならピンと来たかもしれないが、Raspberry Pi Picoの初回設定時にUF2ファイルを書き込むのと似たような作業だ。)

WindowsキーとRキーを同時に押し、"ファイル名を指定して実行"ウィンドウを出し、"cmd"と入力して"OK"をクリックして、コマンドプロンプトを起動する。(コマンドプロンプトのかわりにWindows PowerShellを利用してもよい)

 

コマンドプロンプトの起動

 

コマンドプロンプトに下記のコマンドを打ち込み、フラッシュの内容を削除する。("COM5"部分はそれぞれの環境に応じて変更)

esptool.py.exe --chip esp32c3 --port COM5 erase_flash

フラッシュの消去

続いて下記のコマンドを打ち込み、ファームウェアを書き込む。("COM5"部分は先のポート名に、"UserName\FilePath"部分はダウンロードしたファームウェアの格納されているフォルダのパス、"esp32c3-20220117-v1.18.bin"部分はダウンロードしたファームウェアのファイル名に変更)

esptool.py.exe --chip esp32c3 --port COM5 --baud 460800 write_flash -z 0x0 "C:\Users\UserName\FilePath\esp32c3-usb-20220618-v1.19.1.bin

ファームウェアの書き込み

書き込みには少し時間がかかるが、成功すれば、"Leaving..."というメッセージに続き、"Hard resetting via RTS pin..."というメッセージが表示される。

ファームウェアの書き込み完了

これでM5Stamp C3U側の準備は完了だ。一度M5Stamp C3UをUSBポートから抜いておこう。

Thonny Python IDEの設定

続いて、開発するWindows PC側の設定に入る。マイコンボードを用いた開発は、IDE統合開発環境)を用いるのが一般的だが、今回はRaspberry Piにプレインストールされていることでもお馴染みのThonnyを使うことにしよう。

thonny.orgThonnyの公式サイトからWindows版をダウンロードして、インストールする。

Thonnyのダウンロード

インストールが完了したら、Thonny Python IDEを起動する。

Thonny

Thonnyを起動したら、"Tools"→"Options"と進み、デバイスを"MirroPython (ESP32)"に、ポートを"USB Serial Device (COM5)"に設定し、"OK"をクリックする。("COM5"部分はそれぞれの環境に応じて変更)

 

ThonnyにESP32を設定

これでWindows + MycroPythonによるM5Stamp C3Uの開発環境の構築は完了だ。

 

Lチカで動作確認

配線

さて、M5Stamp C3Uの準備が整ったところで、動作確認をしてみよう。マイコンボードの動作確認の定番といえば、LEDの自動点滅、いわゆるLチカだ。

とりあえずM5Stamp C3Uの3番ピンに330Ω程度の抵抗器とLEDのアノードを直列に接続する。続いて、LEDのカソードをM5Stamp C3UのGND端子に接続する。

 

ブレッドボードの配線例

コーディング・書き込み

配線が完了したら、M5Stamp C3UをUSBケーブルでWindows PCに接続し、下記のコードを入力(もしくはCopy & Paste)する。

import machine
import time
led = machine.Pin(3, machine.Pin.OUT)
while True:
    led.value(1)
    time.sleep(1)
    led.value(0)
    time.sleep(1)

"while True:"以降は最後まで行頭に同じレベルのインデントを入れるのを忘れないように。

ThonnyでLチカのコーディング

入力し終えたら、ファイルをM5Stamp C3Uに書き込もう。

"File"→"Save as"とクリックし、"MicroPython device"を選択する。

 

M5Stamp C3Uに書き込み

"main.py"というファイル名を指定して保存する。

M5Stamp C3Uに書き込み

保存した後、F5キーを押すか、"Run current script"をクリックすれば書き込んだプログラムが動き出し、1秒間隔でLEDが点灯と消灯を繰り返すはずだ。

M5Stamp C3UでLチカ

これでひとまず、M5Stamp C3UをWindows + MycroPython環境で開発する環境が整った。あとは楽しく遊ぶこととしよう。

対称性を意識してAmpereの法則とBiot-Savartの法則を活用しよう

電流と磁場の公式を丸暗記?!

電流と磁場の公式。似たようなものが多くて、いろいろと混乱する。なんか電流が作る磁場の公式としても、

f:id:medicalengineer:20211113233539p:plain

だか

f:id:medicalengineer:20211113233618p:plain

だかわからなくなってくる。

そもそも、電磁気学に限らず自然科学において公式をひたすら丸暗記するなど愚の骨頂だ。Ampere(アンペール)の法則とBiot-Savart(ビオ・サバール)の法則に立ち返ればよい。とはいっても、これらの法則は積分形で書かれているから理解するのも面倒だ。ここでは、厳密さにはちょっとばかし目をつぶって、対称性を最大限に活用しながら、できるだけ簡単に理解できるように工夫しながら考えてゆこう。

 

無限長直線電流が作る磁場:円周で対称性を考える

公式丸暗記からの脱却の第一歩として、無限長直線電流を簡単に導出できるように頭を切り替えよう。これはAmpereの法則を用いて導出することにしよう。Ampereの法則とは、ぶっちゃけた言い方をすれば「ある周回経路に沿って磁場を足し合わせれば、その周回経路を貫く電流に等しくなる」ということだ。つまり、

f:id:medicalengineer:20211113235221p:plain

ということだ。周回積分

の領域を電流を中心とする半径rの同心円状にとれば、

f:id:medicalengineer:20211113235554p:plain

であるから、

f:id:medicalengineer:20211113233618p:plain

を得る。そもそも、無限長直線電流が作る磁場は2次元的に対象な筈だから円の周長2πrで割り算していると考えてもよかろう。

f:id:medicalengineer:20220416132659j:plain

Ampere's law

円環電流の中心の磁場:球面で対称性を考えてから円周にわたって足し合わせる

さて、続いては円環電流の中心の磁場だ。こちらはBiot-Savartの法則を用いて導出しよう。Biot-Savartの法則とは、電流がそのまわりに磁場を作るとき、その磁場の大きさを表す法則だ。本来ならばベクトルを用いた表記をすべきだが、ここでは簡単に理解してもらうため、

f:id:medicalengineer:20211113234026p:plain

と書いておこう。

なんのことはない、微小長さあたりで考えれば、電流に微小導体の長さをかけた値、すなわち微小電荷と速度の積*1を球の表面積で割っているにすぎない。そう、点とみなせる微小電流は3次元的に対称な筈だから球の表面積4πrで割り算している*2とでも考えておけば覚えやすかろう。つまり、Biot-Savartの法則は実質的にCoulomb(クーロン)の法則に電荷の移動速度を乗じたものなのだ。さて、円環電流がその中心につくる磁場Hを求めたければ、上式をおもむろに積分してやればよい。すなわち、

f:id:medicalengineer:20211113233835p:plain

であることがわかる。ただし、微小経路を足し合わせたものが円周となることから、

f:id:medicalengineer:20211113234113p:plain

であることを用いた。

f:id:medicalengineer:20220416152058j:plain

Biot-Savart

今回は無限長直線電流が作る磁場と、円環電流が作る磁場を考えたが、対称性を利用すると考えるのが簡単になる。これら以外にも、対称性を利用して考えると覚えやすい公式も多い。電気工学はもちろん、物理学や数学といった自然科学系分野では、対称性を考えるというのは極めて大切なことである。

 

*1:何故そうなるのか疑問に思う方もいると思うが、

f:id:medicalengineer:20211118234204j:image

と捉えればわかりやすいだろう。

*2:あくまで覚える方法でしかない。電磁気学的にはあまり正確な考えではない。

Coulombの法則の本質

Coulombの法則は何故逆2乗則か?

電荷が作る磁場の計算や電荷同士に働く力の計算でお馴染みCoulomb(クーロン)の法則だが、医用電気工学のテキストでは

f:id:medicalengineer:20211114001404p:plain

と表されることが多い。「点電荷が作る電場は電荷に比例し、距離の2乗に反比例する」という、いわゆる「逆2条則」の典型例である。そもそも何故2乗に反比例するのか考えたことがあるだろうか。単なる反比例や3乗に反比例しちゃダメなのだろうか?

高校物理の教科書や参考書では歴史的な事情からか、

f:id:medicalengineer:20211114003954p:plain

とおいて、

f:id:medicalengineer:20211114001421p:plain

と紹介していることも多いが、もはや発狂モノだ。こんなの丸暗記しても、本質の理解には到底及びそうにない。本質を理解するために、まずは現象を単純に捉えてみよう。

f:id:medicalengineer:20211121233547j:plain

Coulomb's law



本来ならばCoulombの法則は電束密度Dを用いて

f:id:medicalengineer:20211114002915p:plain

と与えられるべきものだ。この式の意味するところをイメージできたなら、この先の理解はきっと早い。

 

勿論、この式は電荷の大きさ(すなわちそこから出る電束の本数)を半径rの球の表面積で除している。これは、「電束密度とは単位面積当たりの電束*1である(電束密度=電束の本数÷面積)」という定義そのものにすぎない。勿論、電束の空間的な広がり方は電荷に対して球対象であるから球の表面積で除しているわけだ。


ここで、電束密度Dを電場Eと誘電率εを用いて

f:id:medicalengineer:20211114002447p:plain

とすれば、

f:id:medicalengineer:20211114002907p:plain

であるから、両辺をεで除して、

f:id:medicalengineer:20211114002957p:plain

を得る。


ちなみに、この考え方を拡張すると、任意の平曲面Sに対しては、

f:id:medicalengineer:20211114005227p:plain

と表すことができる。これはMaxwell(マックスウェル)方程式を構成する方程式の1つ、Maxwell-Gauss(マックスウェルーガウス)の式*2とよばれるものである。

 

電場がわかれば電荷に作用する力はすぐにわかる

当然、電場は「1Cの電荷が受ける力が1Nであるような電場の大きさを1 [N/C] = 1 [V/m]と定める」こと、すなわち電荷Q、電場Eに対し、作用する力が

f:id:medicalengineer:20211121233842p:plain

と与えられるため、電荷同士に作用する力については、「電荷Q1が作る電場がQ2に及ぼす力」を考えれば、自ずと

f:id:medicalengineer:20211114010815p:plain

であることも理解できよう。

 

対称性を考えよう!

物理現象を考察する際に、対称性はとても便利だ。別の機会に「無限長直線電流が作る磁場」と「円環電流の中心の磁場」について紹介するつもりだが、このような電流が作る磁場を考えるときも非常に役に立つ。自然現象を考察するときは、是非とも対称性に着目してみよう。きっと、新たな見方が見つかる筈だ。

*1:+Q [C]の電荷からQ本の電束が湧き出す。

*2:これは積分形とよばれる形式であり、微分形とよばれる形式 Div D = ρ を用いて表現することもある。

Arduino Nano互換ボードでパルスオキシメータ製作

【注意】本記事にて製作したデバイスは薬機法に定める医療機器ではなく、診断を目的としたものでもありません。COVID-19が疑わしき場合は、保健所およびかかりつけの医療機関等にご相談願います。

Notice: This pulse oximetry system is NOT for patient diagnose. In case of suspected COVID-19, you should contact health and medical services.

 

 COVID-19のパンデミックの中、パルスオキシメータに対する注目が高まったのは記憶に新しいと思う。理由は至極単純、肺炎になればSpO2が低下する、すなわちSpO2が重症度の目安として使えそうだというのだ。

 仕事柄、職場に行けばパルスオキシメータなんぞ自由に使える。しかし、問題は自宅で悪化したときだ。今、我が家にはパルスオキシメータなどない。家庭用の安物は精度や確度に難があるとの報告もあった。

 かといって、職場に出入りしている業者さんやメディカルスタッフ向けの通販経由で医療用のパルスオキシメータを買ってしまうと、本当に必要な施設への供給を妨げることになってしまう。メディカルグレードでないにせよ、自宅療養やホテル療養の患者様に貸し出されているとのことなので、既製品を購入するのは躊躇われる。

 さらに、パルスオキシメータというのは家庭用でさえ最低でも1万円程度、そこそこのグレードでは2~3万円程度と結構値が張るのだ。したがって、おいそれとは購入できない。医療現場のため、自分のお財布のため、ちょっと我慢しよう……と思わなくもないが、あったら万一の際に便利だ。

 

 だったら、作ればいいよねっ?!

 

……というわけで、パルスオキシメータを作ることにした。

 ブレッドボードにArduino NanoとMAX30102とSSD1306OLEDディスプレイを載せ、ジャンパワイヤでI2C接続する。たったこれだけのいたってシンプルなシステムだ。

 今回私が使ったのは正確には本家Arduino Nanoではなく、Arduino Nano互換ボードだ。しかし、本家Arduino Nanoと遜色ないレベルでありながら価格も手頃だ。適当にAmazonやら秋月やらRS componentsやらで購入すればよい。参考までに、私が使用したKKHMF製のArduino Nano互換ボードおよび各パーツのAmazonリンクを付しておく。

 

 

 

  今回、私が利用したKKHMF製ボードにおける使用ピンは5V, GND, A4=SDA , A5=SCLだ。 一応、各ピンの用途を説明しておこう。5Vは電源線、GNDはGroundの略で接地(アース)線を、SDAはSerial Dataの略で信号線を、SCLはSerial Clockの略でクロックラインを意味するものだ。

 5Vの電源線は当然のごとく電源供給のためのラインだ。これがないとI2C接続されたデバイスは当然動作しない。

 GNDは0Vの電位基準であるとともに、回路を構成する帰線でもあるため、当然だがこれも接続しないとうまく動作しない。

 シリアル形式のデジタル伝送においては、クロックラインに送られるクロックパルスに同期して信号線の信号を読み書きするのだ。

 

f:id:medicalengineer:20210504221153p:plain

pulseoximeter schema

 さて、接続ができたら、母艦となるPC(今回はRaspberry Pi 4を使用)上のArduino IDE上でソースコードを打ち込む。正確にはArduino言語なのだが、ほとんどC言語と同じような感覚でコーディングできる。どうでもいいけど、Cは直感的なコーディングができるから初学者にとってはわかりやすくていいと思う。参考のため、ソースコードを記しておこう。



// ************************
// Arduino Pulse Oximeter
// ************************


// Include libralies
#include <wire.h>
#include <adafruit_gfx.h>
#include <adafruit_ssd1306.h>
#include <max30105.h>


// Definition for OLED
Adafruit_SSD1306 display(-1);
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels


// Definition for MAX30102
MAX30105 particleSensor;
#define MAX_BRIGHTNESS 255


// Constants for threshold and initial value
const uint32_t THRESHOLD_FINGER_DETECT = 7000;
const int32_t THRESHOLD_PULSE_UP_EDGE = 120;
const int32_t MIN_INIT = 9999999;
const int32_t MAX_INIT = 0;


// Limitation of HR and SpO2
const uint32_t LOWER_LIM_HR = 30;
const uint32_t UPPER_LIM_HR = 180;
const uint32_t LOWER_LIM_SPO2 = 70;
const uint32_t UPPER_LIM_SPO2 = 100;


// Valuables to pulse detect
long l_time = millis();
int32_t prev_irValue = 0;
int32_t prev_diff = 0;
int32_t diff;
long pulse_interval = -1;


// Constants and valuables for moving average
const int FRAME_REF = 20;
const int FRAME_SPO2 = 5;
const int FRAME_HR = 1;

long sum_ir;
long sum_red;
long sum_spo2;
long sum_hr;
int cnt_ref = 0;
int cnt_spo2 = 0;
int cnt_hr = 0;

long data_ir[FRAME_REF];
long data_red[FRAME_REF];
long data_spo2[FRAME_SPO2];
long data_hr[FRAME_HR];

long irValue_dc = 0;
long redValue_dc = 0;
double spo2_avg = 0;
double hr_avg = 0;


// Keep minimum and maximum value
int32_t min_irValue = MIN_INIT;
int32_t max_irValue = MAX_INIT;
int32_t min_redValue = MIN_INIT;
int32_t max_redValue = MAX_INIT;


void setup() {
// put your setup code here, to run once:
display.begin(SSD1306_SWITCHCAPVCC,0x3C);
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.println("Starting System");
display.println("Please Wait");
display.display();
delay(2000);
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.println("Pulse");
display.println("Oximeter");
display.display();
delay(2000);


// Initialize sensor
particleSensor.begin(Wire, I2C_SPEED_FAST); //Use default I2C port, 400kHz speed
particleSensor.setup(); //Configure sensor with default settings
particleSensor.setPulseAmplitudeRed(0x0A); //Turn Red LED to low to indicate sensor is running


byte ledBrightness = 0x1F; //Options: 0=Off to 255=50mA
byte diffmpleAverage = 8; //Options: 1, 2, 4, 8, 16, 32
byte ledMode = 2; //Options: 1 = Red only, 2 = Red + IR, 3 = Red + IR + Green
int diffmpleRate = 400; //Options: 50, 100, 200, 400, 800, 1000, 1600, 3200
int pulse_intervalWidth = 411; //Options: 69, 118, 215, 411
int adcRange = 4096; //Options: 2048, 4096, 8192, 16384

 

// Initialize array for moving average
// For irValue and redValue
int i_ref;
for (i_ref = 0;i_ref < FRAME_REF;i_ref++){
data_ir[i_ref] = 0;
data_red[i_ref] = 0;
}
sum_ir = 0;
sum_red = 0;

// For SpO2
int i_spo2;
for (i_spo2 = 0;i_spo2 < FRAME_SPO2;i_spo2++){
data_spo2[i_spo2] = 0;
}
sum_spo2 = 0;

// For HR
int i_hr;
for (i_hr = 0;i_hr < FRAME_HR;i_hr++){
data_hr[i_hr] = 0;
}
sum_hr = 0;

// Set initial values
prev_irValue = particleSensor.getRed();
l_time = millis();
}

 

void loop() {
// put your main code here, to run repeatedly:
long irValue = particleSensor.getRed(); //Reading the IR value
long redValue = particleSensor.getIR(); //Reading the Red value


// Finger detect
if(irValue < THRESHOLD_FINGER_DETECT || redValue < THRESHOLD_FINGER_DETECT) {
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.println("Put your");
display.println("finger");
display.display();
return;
}


// Moving average
if(cnt_ref == FRAME_REF){
cnt_ref = 0;
}

// For irValue
if(data_ir[cnt_ref] != 0){
sum_ir -= data_ir[cnt_ref];
}
data_ir[cnt_ref] = irValue;
sum_ir += data_ir[cnt_ref];
irValue_dc = sum_ir / FRAME_REF;

// For redValue
if(data_red[cnt_ref] != 0){
sum_red -= data_red[cnt_ref];
}
data_red[cnt_ref] = redValue;
sum_red += data_red[cnt_ref];
redValue_dc = sum_red / FRAME_REF;

// Increment
cnt_ref++;


// Keep minimum and maximum values to calculate irValue_ac and redValue_ac
if(irValue < min_irValue){
min_irValue = irValue;
}
if(irValue > max_irValue){
max_irValue = irValue;
}
if(redValue < min_redValue){
min_redValue = redValue;
}
if(redValue > max_redValue){
max_redValue = redValue;
}

 

// Calculate HR and SpO2 if pulse was detected
diff = prev_irValue-irValue;
if(prev_diff < THRESHOLD_PULSE_UP_EDGE && diff > THRESHOLD_PULSE_UP_EDGE){
// Calculate pulse interval
pulse_interval = millis() - l_time;
l_time = millis();


// Calculate HR
double hr = 60000 / pulse_interval;


// Calculate AC components
int32_t irValue_ac = max_irValue - min_irValue;
int32_t redValue_ac = max_redValue - min_redValue;


// Calculate R
double r = (double(redValue_ac) / redValue_dc) / (double(irValue_ac) / irValue_dc);


// Calculate SpO2 from r
double spo2 = (-45.060 * r * r + 30.354 * r + 94.845);


// Initialize minimum and maximum values
min_irValue = MIN_INIT;
max_irValue = MAX_INIT;
min_redValue = MIN_INIT;
max_redValue = MAX_INIT;


// Moving average for SpO2
if(cnt_spo2 == FRAME_SPO2){
cnt_spo2 = 0;
}
if(data_spo2[cnt_spo2] != 0){
sum_spo2 -= data_spo2[cnt_spo2];
}
data_spo2[cnt_spo2] = spo2;
sum_spo2 += data_spo2[cnt_spo2];
spo2_avg = sum_spo2 / FRAME_SPO2;
cnt_spo2++;


// Moving average for HR
if(cnt_hr == FRAME_HR){
cnt_hr = 0;
}
if(data_hr[cnt_hr] != 0){
sum_hr -= data_hr[cnt_hr];
}
data_hr[cnt_hr] = hr;
sum_hr += data_hr[cnt_hr];
hr_avg = sum_hr / FRAME_HR;
cnt_hr++;


// Show SpO2 and HR
if(hr_avg >= LOWER_LIM_HR && hr_avg <= UPPER_LIM_HR && spo2_avg >= LOWER_LIM_SPO2 && spo2_avg <= UPPER_LIM_SPO2){
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.print(" HR ");
display.println(hr_avg,0);
display.print("SpO2 ");
display.println(spo2_avg,0);
display.display();
}
}
prev_irValue = irValue;
prev_diff = diff;
}

 

 コーディングが終わったら、早速Arduinoボードに書き込もう。書き込んだら
早速性能評価だ。おもむろに電源を入れ(単純にUSBケーブルを接続するだけで良い)、立ち上がりを待つ。計測がうまくいけば、程なく心拍数とSpO2の値がモニタに表示される筈だ。

 今回私が組み上げたものは、心拍数の計測値が不安定で、ごくわずかな体動でも150を超える値を吐いてくる。移動平均のフレーム数や立ち上がり検出の閾値といったパラメータを調整するなり、微分を実装するなり、改善の余地がありそうだ。しかし、SpO2は結構良かった。職場のメディカルグレードのパルスオキシメータで正確さ(確度)を確認してみたところ、この自作パルスオキシメータは、メディカルグレードのパルスオキシメータとほぼ同じ値を示した。なかなか良いじゃないか、これ。

f:id:medicalengineer:20210504221156j:plain

 医用工学に足を突っ込んだばかりの学生でも、これならきっと簡単だ。パルスオキシメータはCOVID-19パンデミックが収束したとしても、様々な用途がある便利なアイテムだ。ぜひともトライしてみてほしい。

A-aDO2(肺胞気動脈血酸素分圧較差)に関する物質収支の考え方

 A-aDO2(肺胞気動脈血酸素分圧較差)とは

 肺胞から血液への酸素の拡散能が低下すると、肺胞に酸素が届いていても血液に酸素が受け渡されず、全身の酸素需給が悪化する。そこで、肺胞から血液への酸素の拡散能を反映する指標として、肺胞気酸素分圧と動脈血酸素分圧の較差(A-aDO2)を考えてみよう。

 

 まずは単純に

A-aDO2=肺胞気酸素分圧-動脈血酸素分圧

と定義しよう。もちろん、肺胞から血液に酸素が移動できないと肺胞酸素分圧は上昇し、動脈血酸素分圧は低下するため、肺胞から血液への酸素の拡散能が低下すると、A-aDO2は開大する

 

 肺胞における物質収支計算


 さて、A-aDO2を実際に計算してみよう。動脈血酸素分圧は採血すれば簡単にわかるが、肺胞気酸素分圧はそう簡単にはわからない。酸素が肺胞から血管に拡散してしまう(ただし拡散能は低い)ため、肺胞気酸素分圧は吸入気酸素分圧よりも低下してしまうからだ。すなわち、

肺胞気酸素分圧=吸入気酸素分圧-肺胞から毛細血管に移動した酸素量

である。


 そこで、動脈血二酸化炭素分圧(採血すれば簡単にわかる)を利用して推定してみよう。酸素と異なり、二酸化炭素は拡散能が高く、肺胞毛細血管の二酸化炭素分圧と肺胞気の二酸化炭素分圧はほぼ等しく(平衡状態)なっている。また、通常の代謝では、酸素1molの消費に対して、二酸化炭素0.8mol程度が排出されている。生物学では、消費した酸素の物質量に対する排出された二酸化炭素の物質量の比のことを呼吸商(RQ)といい、通常の代謝状態ではRQ≒0.8である。もちろん、呼吸商の定義から、

組織で消費された酸素量=動脈血二酸化炭素量÷呼吸商

である。

 

 ここで、定常状態であれば、組織で消費された酸素量は、肺胞から毛細血管に移動した酸素量と等しいため、

肺胞気酸素分圧=吸入気酸素分圧-肺胞から毛細血管に移動した酸素量

すなわち

肺胞気酸素分圧=吸入気酸素分圧-動脈血二酸化炭素÷呼吸商

であることがわかる。


 あとは、肺胞気酸素分圧がわかればよい。ここで、肺胞気は湿度100%であり、水蒸気で飽和しているため、大気圧(760mmHg)から飽和水蒸気圧(37℃では47mmHg)を引いた分圧が吸入気の空気分圧となる。この値に吸入気酸素濃度FIO2をかけてやれば、吸入気酸素分圧がわかる。
ゆえに、
A-aDO2=(760-47)×FIO2-(PaCO2÷RQ)-PaO2
が示された。

 

f:id:medicalengineer:20210204000611j:plain

A-aDO2





 

地方発着 0泊3日弾丸台湾旅行

2019年も残り数日となったところで、また台湾に行ってしまった。

だって安かったんだもん。

 

とりあえずいつものように地元から高速バスに乗って東京へ。もはやプライベートで海外旅行するときは東京まで高速バスで出るのが定番となってしまった。

f:id:medicalengineer:20210810225700j:image

今回もWILLER EXPRESSの定番シート、リラックスを利用。
f:id:medicalengineer:20210810225709j:image

高速バスは途中のサービスエリアやパーキングエリアで休憩を挟みながら東京駅へ向かう。実は、この往路の高速バスが今回の旅程で最も長時間乗り続ける乗り物だったりする。「海外旅行の最大の関門は国際線の飛行機」という常識は、私にはもはや通用しない。最大の関門は成田空港までの移動だ。それは時間的にも、費用的にもだ。そもそも私の場合、海外旅行の費用の大部分が、日本国内でかかる費用なのだ。いろいろと頭がおかしいと言われそうだが、細かいことは気にしていられない。海外を楽しめればそれでいいんだよ。

都内の首都高で渋滞に巻き込まれながらも、バスは7時間かけて東京駅に到着。
f:id:medicalengineer:20210813222409j:image

年の瀬でごった返す東京駅を足早に駆け抜ける。目指すは東京駅八重洲口から少し歩いたところにある京成バスのバス乗り場だ。ここから東京シャトルに乗って成田空港第3ターミナルへ。短時間で高速バス同士の乗り換えも面白いものだ。
f:id:medicalengineer:20210810225704j:image

時間に余裕をもって空港に着いたわけだし、早めにチェックインしてしまおう……とはいかない。何故ならば、チェックイン開始時刻は出発時刻の3時間前と決まっており、それ以前はシステムが受け付けてくれないからだ。

f:id:medicalengineer:20210810225447j:image

チェックインは乗客自身で行うようになっている。セルフサービスはLCCの基本だ。PAXの立場としても、カウンターでの受付よりも気楽でいい。
f:id:medicalengineer:20210810225821j:image

シートアサインは比較的前方のウィンドウサイド。早めにチェックインしたのが功奏したか、有料オプションを何一つ使わずにこのいいポジションをアサインしてもらえた。いや、たぶん単なる偶然だと思うけど。チェックインが済んだら、T2へ移動し、IASS EXECUTIVE LOUNGEでゆったりする。

f:id:medicalengineer:20210810225818j:image

ついでに、ここでスマートフォンのSIMを入れ替えておく。私が使っているのはシンガポールの通信大手StarHubが展開しているStarHub Happy  Prepaid SIMを利用している。これはシンガポール出張の際に現地StarHub Shopで手配したものだ。シンガポール国内でデータ通信を格安で利用できる。しかも、特定の国々(日本、台湾、アメリカ等)では、シンガポール国内と同じレートでデータローミングができるプランがあるのだ。もはや弾丸海外トラベラー必携のアイテムと言っても過言ではない。

 

www.starhub.com

ラウンジでゆったりしたあとは、T2内で軽く晩御飯を済ませる。T3へ戻り、チェックインの列を横目にパスポートコントロールへ。

f:id:medicalengineer:20210810225450j:image

パスポートコントロールを通過、ボーディングゲートへ向かう。T1やT2と違って、T3には免税店もそれほど多くはない。だが、割り切った格安旅行ならばそれで十分だ。どうせ免税店でブランド品なんて買うつもりはないでしょ?どうでもいいけど、このT3利用をもって、この1年間に成田のT1からT3まで全て利用したことになる(6月と11月に出張でT1利用(インター&ドメ)、9月にプライベートでT2利用(インターのみ)、今回のT3利用(インターのみ))。東京在住じゃないのに、どれだけ成田から海外行ってるんだよ……?

f:id:medicalengineer:20210810230125j:image

ボーディングブリッジを使わず、ランプバスでシップへ向かい、タラップを上ってMM627便に搭乗。

f:id:medicalengineer:20210810225952j:image

深夜発のインターは実に面白い。たとえ近距離便だとしても旅の高揚感が抑えきれない。はやる心を抑えて、いよいよMM627便に搭乗だ。A320の軽快なエンジン音を聞きながらテイクオフ。天候が良いのか、千葉の街の灯りがはっきりと見えた。離陸後20分程して機内が減光され、さらに10分程してベルト着用サインが消灯。どうやら水平飛行に入ったようだ。上空で腕時計の針を1時間巻き戻す。国際線ならではの楽しみだ。フライトタイムは4時間半。それにしても成田から桃園は近いものだ。だって4時間半のフライトだもん。何度も言うようだが、地元から東京駅までの高速バスが7時間、成田空港から桃園空港までのフライトが5時間ってどういうことだよ……

キャプテンから機内アナウンス。桃園空港の天候が優れず、状況によっては那覇に降りるかもしれないとのこと。万一那覇に降りたらせっかくの台湾旅行が台無しだが、それはそれで興味がある。著者はまだ沖縄に行ったことがない上に、パスポートコントロールを通過したのに国内に降りるとどういう扱いになるのか興味が尽きない。

着陸30分前のアナウンスが入る。程なくしてベルト着用サインが点灯、機内が明るくなり、ファイナルアプローチのアナウンスも入る。

台湾の街の灯りが見えてきて、桃園空港にランディング。とりあえず天候の問題はクリアできたようだ。那覇に心惹かれる部分はあったが、折角インターのチケットを取ったのだ。やっぱり海外を満喫したい。

シップは桃園空港B9にスポットイン。LCCは桃園空港ではT1とT2の境界付近の搭乗口を利用することが多い。チェックインバゲージがなければ、どちらへ出ても問題はない筈なのだが、今回は案内に従ってT1へ向かうこととした。このままパスポートコントロールを抜けてしまってもいいのだが、深夜に制限エリア外に出てもあまりいいことはない。まだ現地時間で深夜2時だ。しばらくは空港内で過ごすのが得策だろう。國光客運(Kuo-Kuang Bus)1819系統の高速バスは24時間運行なので、これに乗れば台北市内まで行こうと思えば行けるのだが、1時間もせずに着いてしまう。当然、夜の3時に台北車站(Taipei Main Station)に降ろされても困る。そこで、空港の制限エリア内で時間を潰すという選択をした。桃園空港はパスポートコントロールに行く手前で、Rest areaがあり、時間を潰すことができるのだ。今回はここで4時半頃まで待つことにしよう。

4時半前にRest areaを出発。いよいよ台湾入国だ。

f:id:medicalengineer:20210810230248j:image

パスポートコントロールを通過する。この時間帯は並んでいる人もおらず、通過はかなりスムーズだ。というか、一般レーンがクローズされていたせいか、"Diplomat, Official, Crew"レーンに通された。なんだこの特別感は……いや、一般レーンと同じだけど。制限エリア内で外貨両替を済ませてから、制限エリア外へ。既にStarHub Prepaid SIMを仕込んでいるわけだから、わざわざSIMカードを手配する必要もない。そのままバスターミナルへ向かおう。

5時00分発の國光客運1819系統の高速バスに乗る。もう何度も乗った國光客運1819系統、勝手はわかっている。
f:id:medicalengineer:20210810230007j:image

早朝で道路がすいていたこともあってか、僅か35分で台北車站に到着した。さて、台北に到着したことだし、朝ごはんでも食べようか……よし、目指すは華山市場のフードコートだ。MRT(地下鉄)に乗っても良さそうだが、まだ早いので、とりあえず歩いて向かうことにしよう。台北駅から華山市場までは歩いて20分程度だ。華山市場に到着すると、早朝にもかかわらず、長い行列ができている。とりあえずこの大行列に並ぼう。お目当ては華山市場2階のフードコートにある阜杭豆漿(Fu Hang Dou Jiang)という朝食のお店だ。ここではもはや定番と言ってもいいくらいの有名メニュー、鹹豆漿をいただく。鹹豆漿(Xian Dou Jiang)とは、温めた豆乳を酢で固めた料理で、これがなかなか美味しいのだ。

f:id:medicalengineer:20210812222207j:image

Ikari Coffeeという洒落たカフェがあったので、思わず入ってしまった。
f:id:medicalengineer:20210812222204j:image
洒落た内装の店内でカフェラテをいただく。美味しいカフェラテでナイトフライトの疲れを癒すことにしよう。

f:id:medicalengineer:20210812222200j:image

カフェラテを飲んでゆったりとしたら、台北市内散策再開だ。まずは華山1914文化創意産業園区へ。台湾の歴史とアートが融合した素敵なエリアだ。

f:id:medicalengineer:20210812222340j:image

歴史を感じる赤レンガ造りの建物が美しい。

f:id:medicalengineer:20210813175551j:image

季節柄もあって、いい感じにクリスマスのデコレーションがなされている。

f:id:medicalengineer:20200202132018j:image

ムーミンのキャラクターショップもあった。

f:id:medicalengineer:20200202132053j:image

何故か只見線新潟県小出駅福島県西若松駅)の写真展もやっていた。日本から台湾に来てまで只見線というのも興味深い。
f:id:medicalengineer:20210812222337j:image

ウイスキーの商品名のオブジェクトが青空に映えていた。

f:id:medicalengineer:20200202132025j:image

街中を散策がてら、仁愛敦南円環(Renai Dunnan Traffic circle)へ。日本では考えられないような巨大なラウンドアバウトだ。

f:id:medicalengineer:20210813173248j:image

あまりにデカすぎて、地上から撮影してもラウンドアバウトだとは認識できない。デカすぎてラウンドアバウトなのに信号機が稼働している。日本国内ではラウンドアバウトが交通量が比較的少ない道路同士の交差点に採用されているのとは対照的だ。

f:id:medicalengineer:20210813173340j:image

仁愛敦南円環のすぐそばに誠品書店(eslite)敦南店がある。代官山のTSUTAYAのモデルになったともいわれるお洒落書店の代名詞。
f:id:medicalengineer:20200202132049j:image

さらに東を目指して歩いて行くと、台北市役所(台北市政府)がある。

f:id:medicalengineer:20210811232615j:image

台北市役所付近から南を望むと、台北101がそびえ立つのがはっきりと見える。いや、高すぎて最上部は霧で霞んでしまっていたけど。

f:id:medicalengineer:20200202132034j:image

結局、台北駅から台北101まで歩いてしまった。特に遠くまで歩いてやろうとか意識したわけではないのだが、気がついたら結構な距離(約5.6km)を歩いてしまっていた。思いのままに気ままな散策。これぞ一人旅の醍醐味だ。

さて、台北の見どころはまだまだある。流石にこれからまた歩き回るのも大変なので、今度はMRTに乗って移動しよう。乗換えで忠孝新生(Zhongxiao Xinsheng)駅に降りた。地下空間に広がるこの開放感あふれるプラットホームは、なかなか素敵な都市デザインだ。

f:id:medicalengineer:20210811232743j:image

『台湾の原宿』といわれる西門町(Ximending)へ。さて、ここからは食べ歩きを楽しもう。まずは阿宗麺線(Ay-Chung Flour-Rice Noodle)で麺線だ。

f:id:medicalengineer:20210813224858j:image
麺線は台湾のそうめんのようなもので、阿宗麺線の麺線は鰹だしの利いたスープに滑らかな麺線とモツが入っていて、優しい味わいだ。そのままでも勿論美味しいのだが、テーブルに置いてある黒酢やチリソースなどで味変もでき、色々な味を楽しめるので、リピーターにもお勧めだ。

続いてはタピオカミルクティーの幸福堂(Xing Fu Tang)だ。台湾に来たからにはタピオカミルクティーだ。パスポートが必要な本気のタピ活。
f:id:medicalengineer:20200202132022j:image

並んでいて待ち時間もそこそこあったけど、待つだけの甲斐はあった。なかなか美味しい。
f:id:medicalengineer:20200202132030j:image

そうこうしているうちに日が暮れた。夜の西門町は活気があって楽しいところだ。

f:id:medicalengineer:20210812222500j:image

MRT西門(Ximen)駅6番出口付近は、日本でいうところの渋谷駅スクランブル交差点のようなもので、若者たちでごった返している。

f:id:medicalengineer:20210813182614j:image
f:id:medicalengineer:20210812222457j:image

MRT東門(Dongmen)駅に移動し、東門駅付近の永康街(Yongkang Street)を散策。台湾茶と焼菓子の専門店、小茶栽堂に立ち寄る。スタイリッシュでお洒落なショップだ。小茶栽堂のマカロンヌガーは、カラフルでかわいらしくて美味しいので、台湾土産にお勧めだ。

f:id:medicalengineer:20210813175725j:image

まだ台北を楽しみたい気持ちもあるが、今回は気軽な日帰り旅行だ。早めに帰る準備をしよう。MRTに乗って台北車站(Taipei Main Station)に出て、晩御飯。日本でもお馴染みの台湾グルメ、魯肉飯の定食をいただくことにした。

f:id:medicalengineer:20210813175715j:image

台北車站から國光客運1819系統の高速バスに乗って桃園空港へ。
f:id:medicalengineer:20210813175721j:image

バス車内では、コンビニで買った烏龍茶を楽しむ。

f:id:medicalengineer:20210813175433j:image
地方在住者の弾丸台湾旅行であれば、桃園空港には早めに到着すべきだ。何故ならば、桃園空港の国際線制限エリアにはAirport Experience Zoneという簡易ラウンジがあり、そこでは無料でシャワーを浴びることもできるからだ。1日の汗を流し、着替えを済ませ、ナイトフライトに備えよう。

シャワーで1日の汗を流したら、あとは日本へ帰るだけだ。長い廊下を歩いてボーディングゲートへ。

f:id:medicalengineer:20210813182544j:image

台湾への名残を惜しみつつ、B9ゲートからMM620便に搭乗。

f:id:medicalengineer:20210811232946j:image

成田に向けてテイクオフ。フライト時間は短いので、軽く仮眠といったところ。LCCの短距離国際線は気楽でいい。LCC機内食もないので、離陸したらすぐに寝てしまっても構わない。さらに、入国書類なんざ機内でもらわなくても、パスポートコントロール入口に山ほど置いてあるし、旅慣れてくると日本入国用の税関申告書は手元にあるというケースも多いだろう。なんなら税関のWebサイトからダウンロードして印刷することだって可能だ。夜の短距離国際線は、いかに睡眠時間を確保するかが勝負なのだ。

気がついたら雲のむこうから朝日が昇ろうとしていた。なんて美しい景色なんだろう。
f:id:medicalengineer:20210811232949j:image

機内が慌ただしくなってくると、いよいよ成田空港に着陸だ。

T3に到着。早いところパスポートコントロールを通過してしまおう。

f:id:medicalengineer:20210812222607j:image

パスポートコントロールを通過したら、T3からそのまま東京駅方面へ向かうバスに乗ってもよいのだが、ここは敢えてT2に向かう。

f:id:medicalengineer:20210812222600j:image

T2のIASS EXECUTIVE LOUNGEをアライバルラウンジとして利用させてもらい、しばし荷物整理とスマートフォンのSIMの入れ替え。落ち着いたら、バスで東京駅へ向かう。空港を出て、バスのシートで仮眠をとろう。気がつくと東京駅のすぐそばだった。成田空港から約1時間、東京駅八重洲口に到着だ。さて、帰りの高速バスは夕方だ。このあたりで少し遊ぶことにしよう。

まずは東京駅丸の内側へ。晴れ渡った青い空と緑の芝生に東京駅の赤レンガ駅舎が映える。
f:id:medicalengineer:20210812222557j:image

丸ノ内線半蔵門線東横線京浜東北線を乗り継いで桜木町へ。弾丸海外旅行にハマる前は、東京や横浜は「遊びに行く目的地」であり、来るたびにワクワクが止まらなかったものだが、弾丸海外旅行をするようになってからはもはや「帰ってきた」感覚でしかなくなってしまった。まあ、それでも楽しいことに変わりはないからいいんだけど。
f:id:medicalengineer:20210812222604j:image

汽車道を歩いてみなとみらい地区を満喫。

f:id:medicalengineer:20210813182849j:image

横浜赤レンガ倉庫ではクリスマスイベント開催中だった。
f:id:medicalengineer:20210813182846j:image

そのまま山下公園まで散策。年の瀬の山下公園はいいものだ。
f:id:medicalengineer:20210813182853j:image軽く横浜周辺で遊びまわったのち、地元へ帰る高速バスへ。

自宅の出発から帰着までを振り返ると、国内にいた時間の方が長いくらいの旅程だった。もはや海外旅行なのかと問い詰めたいところだが、自分自身としては大満足だ。

とにかく激安、弾丸海外旅行を是非ともお勧めしたい。