🌙

Tera Term依存者がRaspberry PiでUART通信を確認するのに困った話

2025/01/12に公開

Raspberry Pi OSUART

本記事は、Raspberry Piに於けるUART通信について、私の手元にある情報を纏めたものです。本題に至るまでが少し長いのでご注意下さい。

結論をここに述べると、二点になります。

  • 設定を頑張りましょう
  • VSCodeを使いましょう

私の見聞きしたところによれば、Tera TermWindowsにしかないそうですが、本当でしょうか。WindowsからLinuxへの遠隔ログイン(ssh接続)が主な活躍の場である一方、もう一つ、「シリアルモニター」としての役割も持ちます。

Tera Term画面
Tera Term新規接続画面(上部が遠隔ログイン。下部がシリアルモニター。)

シリアルモニターは、UART通信の様子を目視するのに便利なものです。大抵のものは、受信した内容を都度自動で表示して見せてくれるほか、こちらからキーボード入力して送信することもできます。

プログラミング言語でもシリアルモニターの代用は可能ですが、当然乍らプログラムを書かなければなりません。加えて、受信する際には逐一データを取得して表示させなければならず、ただ人間が閲覧する用途に於いては不便です。

以前、Raspberry Pi 4Bで無線通信を制御する機会がありました。この時使っていた無線通信モジュールの制御には、UARTが必要だったのです。
無線でデータ送信するには、UART通信でコマンドを送る。無線でデータ受信するには、UART通信でデータが送られてくるのを読み取る⋯。最終的には、プログラムでこれができなければなりません。

Raspberry PiといえばPythonPythonといえばpyserialUART通信を行うことができます。とは言え、初めて使うものをいきなりプログラムで制御するのは無謀というものです。まずはシリアルモニターで使い方を確認する必要がありました。

今でこそ、シリアルモニターを使えばよいと分かりますが、当時の私はTera Term以外の方法を知らなかったために、大変徒に時間を浪費してしまいました。

本記事では、あれから知った代替案を紹介します。

Raspberry Piの事前準備

シリアルモニターを紹介したとて、シリアル通信(UART通信)が許可されていなければ意味がありません。SDカードへRaspberry Pi OSを書き込んだ際、シリアルコンソールによるログインのための設定をしていると、起動当初からシリアル通信ができる状態となります(多分)。そのような設定をしていない場合は、後からシリアル通信を有効にする必要があります。

Raspberry Piの設定

しばしば、Raspberry Pi OSはバージョン差が問題となることがあります。私の環境は次に示します。

lsb_release
$ lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description:    Debian GNU/Linux 12 (bookworm)
Release:        12
Codename:       bookworm

この通りとしか言いようがありませんが、一目見て分かりやすいのは、コードネームがbookwormであることです。一つ前のコードネームはbullseyeでした。これより古い環境では手順が異なったり、画面内の項目が異なっていたりする場合があります。将来の新たな環境についても同様です。こればかりは、ご利用の環境に適った方法を探すしかありません。

設定の開き方
設定→Raspberry Piの設定

シリアルポートを有効にする
インターフェイス→シリアルポートが無効になっている場合は、有効にする

上のスクリーンショットでは、「シリアルポート」と「シリアルコンソール」の両方を有効にしています。この状態では、シリアルコンソールへのログインが有効になります。かなり雑に言うと、USB経由でログインできるということです。
これに伴い、プログラムでシリアル通信を行おうとする際、「権限」を要することになります。面倒な上、今回はシリアルコンソールでログインする用事もありませんから、シリアルコンソールを無効にしてみます。

シリアルコンソールを無効にする
シリアルコンソールのみ無効にする

設定に変更がある場合は再起動することになります。

再起動

少し補足

上のような画面上の手順ではなく、コマンドsudo raspi-configによって設定することがあります。特に、デスクトップがない状況下(sshログイン時やRaspberry Pi OS Lite)では、こちらで設定することになります。また、こちらにしかない設定項目もあったりなかったりします。

上下キーで3 Interface Optionsを選択し、エンターキーで決定します。

3 Interface Optionsを選択する
Raspberry Pi 4 Model B Rev 1.5, 8GB








                             ┌─────────────────────────────┤ Raspberry Pi Software Configuration Tool (raspi-config) ├──────────────────────────────┐
                             │                                                                                                                      │ 
                             │                           1 System Options       Configure system settings                                           │ 
                             │                           2 Display Options      Configure display settings                                          │ 
                             │                           3 Interface Options    Configure connections to peripherals                                │ 
                             │                           4 Performance Options  Configure performance settings                                      │ 
                             │                           5 Localisation Options Configure language and regional settings                            │ 
                             │                           6 Advanced Options     Configure advanced settings                                         │ 
                             │                           8 Update               Update this tool to the latest version                              │ 
                             │                           9 About raspi-config   Information about this configuration tool                           │ 
                             │                                                                                                                      │ 
                             │                                                                                                                      │ 
                             │                                                                                                                      │ 
                             │                                                                                                                      │ 
                             │                                                                                                                      │ 
                             │                                  <Select>                                  <Finish>                                  │ 
                             │                                                                                                                      │ 
                             └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 
                                                                                                                                                      








I6 Serial Portを選択。

I6 Serial Portを選択する









                             ┌─────────────────────────────┤ Raspberry Pi Software Configuration Tool (raspi-config) ├──────────────────────────────┐
                             │                                                                                                                      │ 
                             │                        I1 SSH         Enable/disable remote command line access using SSH                            │ 
                             │                        I2 RPi Connect Enable/disable Raspberry Pi Connect                                            │ 
                             │                        I3 VNC         Enable/disable graphical remote desktop access                                 │ 
                             │                        I4 SPI         Enable/disable automatic loading of SPI kernel module                          │ 
                             │                        I5 I2C         Enable/disable automatic loading of I2C kernel module                          │ 
                             │                        I6 Serial Port Enable/disable shell messages on the serial connection                         │ 
                             │                        I7 1-Wire      Enable/disable one-wire interface                                              │ 
                             │                        I8 Remote GPIO Enable/disable remote access to GPIO pins                                      │ 
                             │                                                                                                                      │ 
                             │                                                                                                                      │ 
                             │                                                                                                                      │ 
                             │                                                                                                                      │ 
                             │                                                                                                                      │ 
                             │                                  <Select>                                  <Back>                                    │ 
                             │                                                                                                                      │ 
                             └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 
                                                                                                                                                      








ログインシェルについて問われます。先に触れたシリアルコンソールへのログイン(明確に決まった呼称はない?)の話です。ネットワークが無くとも、物理的に線で接続することで、UARTによってログインできるようになるものです。誰でも勝手にジャンパー線を繋いでログインできますから、一応セキュリティー上の脆弱性を生むものです。先述の通り、権限の要求が面倒なので、無効にします。

いいえを選択し、シリアルコンソールを無効にする








                                                           ┌──────────────────────────────────────────────────────────┐
                                                           │                                                          │ 
                                                           │ Would you like a login shell to be accessible over       │ 
                                                           │ serial?                                                  │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │               <はい>                 <いいえ>            │ 
                                                           │                                                          │ 
                                                           └──────────────────────────────────────────────────────────┘ 
                                                                                                                        







いいえを選択した場合、シリアルポートについて問われます。こちらも無効にしてしまうとUARTそのものが使えなくなるので、有効にします。

はいを選択し、シリアルポートを有効にする








                                                           ┌──────────────────────────────────────────────────────────┐
                                                           │                                                          │ 
                                                           │ Would you like the serial port hardware to be enabled?   │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │               <はい>                 <いいえ>            │ 
                                                           │                                                          │ 
                                                           └──────────────────────────────────────────────────────────┘ 
                                                                                                                        







設定した内容が表示されます。

設定内容の確認








                                                           ┌──────────────────────────────────────────────────────────┐
                                                           │                                                          │ 
                                                           │ The serial login shell is disabled                       │ 
                                                           │ The serial interface is enabled                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                                                          │ 
                                                           │                          <了解>                          │ 
                                                           │                                                          │ 
                                                           └──────────────────────────────────────────────────────────┘ 
                                                                                                                        







設定が終ると、始めの画面に戻ります。

右キーでSelectにカーソルが移動するので、左右でSelectFinishを選択できるようになります。Finishで設定画面を閉じます。GUIと同様、設定内容に変更がある場合は再起動します。

設定の確認

再起動後、設定した通り有効になったかどうか確認します。

視認

様々な情報がありますが、本記事では/dev/ttyS0というシリアルポートを利用します。一応その探し方を示しますが、興味なければ飛ばしてください。

rootフォルダー(/記号で表される)のもとにdevフォルダーが存在します。

rootフォルダーの中の様子
$ ls /
bin  boot  dev  etc  home  ⋯ (中略)

この/devの中には、ttyというファイルが幾つも存在します。多すぎるので、ttySを含むものに絞り込みます。

ttySを含むものだけ抽出する
$ ls /dev | grep ttyS
ttyS0

シリアルポートが有効になっていないと、ttyS*は存在しないため、何も出てきません。これで、有効になったことを確認できます。

通信テスト

Raspberry PiGPIOピンの内、UARTに割当てられているのはGPIO14GPIO15です。

GPIO
GPIO一覧(引用:https://deviceplus.jp/raspberrypi/raspberrypi-gpio/)

Transmit(TXD)が送信側、Receive(RXD)が受信側です。単にこの二つのピンを接続することで、自分で送ったデータが自分に返ってきます。

配線できたら、Pythonで確認しましょう。

icecreampip install icecreamなどでインストールできますが、無視してprintに置換しても構いません。見好いと感ぜられたら是非お使いください。

pip installでエラーが出た場合
error: externally-managed-environment

× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
    python3-xyz, where xyz is the package you are trying to
    install.
    
    If you wish to install a non-Debian-packaged Python package,
    create a virtual environment using python3 -m venv path/to/venv.
    Then use path/to/venv/bin/python and path/to/venv/bin/pip. Make
    sure you have python3-full installed.
    
    For more information visit http://rptl.io/venv

note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this, at the risk of breaking your Python installation or OS, by passing --break-system-packages.
hint: See PEP 668 for the detailed specification.

ある程度新しい環境であれば、このようなエラーが出るはずです。僭越乍ら、この対処を記事として御座います。

そうでなくとも、PEP 668と検索すれば充分な情報が見つかります。

頓着なされないならば、pip install icecream --break-system-packagesでインストールできます。

UART通信テスト
import time
import serial
from icecream import ic

# シリアルポート
PORT = '/dev/ttyS0'
# ボーレート
BAUDRATE = 115200

# シリアル通信初期化
with ic(serial.Serial(PORT, BAUDRATE, timeout=0.5)) as uart:
    ic(''' 改行コードが無い場合 ''')
    # 送信
    ic(uart.write(b'serial'))
    # 一応待機
    time.sleep(0.1)
    # 受信
    while ic(uart.read()):
        # 一応待機
        time.sleep(0.1)

    ic(''' 改行コードを入れる場合 ''')
    # 送信
    ic(uart.write(b'serial\n'))
    # 一応待機
    time.sleep(0.1)
    # 受信
    ic(uart.readline())

御存じの通り、python ファイル名.pyで実行できます。

実行結果
ic| serial.Serial(PORT, BAUDRATE, timeout=0.5): Serial<id=0x7f83dbe710, open=True>(port='/dev/ttyS0', baudrate=115200, bytesize=8, parity='N', stopbits=1, timeout=0.5, xonxoff=False, rtscts=False, dsrdtr=False)
ic| ' 改行コードが無い場合 '
ic| uart.write(b'serial'): 6
ic| uart.read(): b's'
ic| uart.read(): b'e'
ic| uart.read(): b'r'
ic| uart.read(): b'i'
ic| uart.read(): b'a'
ic| uart.read(): b'l'
ic| uart.read(): b''
ic| ' 改行コードを入れる場合 '
ic| uart.write(b'serial\n'): 7
ic| uart.readline(): b'serial
                     '

文字列ではなく、バイナリーデータ(バイト列とも)を送受信している点にご注意下さい。例に、'serial'ではなく、b'serial'となっています。
また、最後の行の表示がやや崩れおりますが、改行コード\nによって改行されているだけです。

プログラム概説と余談

詳細な説明は致しませんが、簡単に概要に触れておきます。なおicはデバッグ用についているものですので、以下には省略しています。


初期化については、シリアルポートを間違えないようにすれば(とりあえず)問題ありません。ボーレートは、よくある115200という値を使用しています。本来は、制御対象のボーレートに合わせる必要があります。データシートを読みましょう。

withは、Pythonでよく行われる記法です。

with serial.Serial('/dev/ttyS0', 115200, timeout=0.5) as uart:
    ...

次に同じ意味となります。

uart = serial.Serial('/dev/ttyS0', 115200, timeout=0.5)
...
uart.close()

withを使った記述では、close()を書く必要がなくなります。楽ですね。


プログラムを御覧の通り、二つの方法を使っています。これが全てではありませんが、参考までにご紹介します。

  1. 改行コードがない場合

個人的にこのやり方は推奨しませんが、単純なので使用しています。

uart.write(b'serial')
uart.read()

この場合、送信されるデータがb'serial'であるのに対し、受信されるデータは、一文字目(1バイト目)のb's'のみであります。今一度、上の実行結果を見ると、一文字ずつ表示されていることに納得頂けるでしょう。

次のようにすることで、一応全てのデータを受け取ることができています。

while uart.read():
    ...

uart.read()が「データを受信している間」、この処理が繰り返されるようになっています。

然らば「データを受信しなかった場合」、どうなるでしょうか。

serial.Serial(PORT, BAUDRATE, timeout=0.5)

timeout=0.5とあるのは、「0.5秒間データを受信しなかった場合」タイムアウトとし、受信を止めるようにするものです。timeoutを指定しないと、データを受信するまで、頑として処理の流れを防禦するため、いつまで経ってもプログラムが終わらなくなることがあります。

「終わられると困る場合」は指定せず、「終わらないと困る場合」は指定することになります。


  1. 改行コードがある場合

改行コードがある場合、行単位で一挙にデータを受信できるようになります。

uart.write(b'serial\n')
uart.readline()

readline()は、改行コードが見つかるまでデータ受信を続けます。結果として、serialという文字列の連結を保持したまま受信できています。基本的にはこちらの方が便利ではないでしょうか。


蛇足になりますが、画像のようなバイナリーデータを扱う際には不都合を生じます。
画像によっては、改行コードと同じ値が混入します。readline()は改行コードが見つかるまで勝手に受信してくれる一方、改行コードが見つかると勝手に受信を終えてしまいます。

>>> uart = serial.Serial(PORT, BAUDRATE, timeout=0.5)
>>> uart.write(b'\x11\x22\n\x33\x44')
5
>>> uart.readline()
b'\x11"\n'
>>> uart.readline()
b'3D'
>>> uart.readline()
b''

b'\x11\x22\n\x33\x44'というバイナリーデータを送信し、readline()で受信します。\x11を除いて表示が変化していますが、文字コードとして解釈され、文字に変換されているに過ぎません。

>>> b'\x21'
b'!'
>>> b'\x22'
b'"'
>>> b'\x23'
b'#'

>>> b'\x32'
b'2'
>>> b'\x33'
b'3'
>>> b'\x34'
b'4'

>>> b'\x43'
b'C'
>>> b'\x44'
b'D'
>>> b'\x45'
b'E'

改行コードによる中断を防ぎ、一挙に受信したい際は、readlines()を使うのが簡単です。

>>> uart.write(b'\x11\x22\n\x33\x44')
5
>>> uart.readlines()
[b'\x11"\n', b'3D']

以上、UARTが有効であることを確認しました。

本題

ここまで来て漸く、シリアルモニターを考える段階に至りました。

Arduino IDE

先んじて、需要の狭いものから済ませましょう。

シリアルモニターはArduino IDEにも搭載されています。Arduino開発される方であればよく使用されるでしょう。こちらはWindowsMacLinuxそれぞれに対応しているようです。

Arduino開発用のアプリケーションですから、あくまでも、「Arduino開発の中でシリアルモニターを使える」というものです。大変便利なものではありますが、シリアル通信に対応する機器が全てArduino IDEでどうにかなるというものではありません。その上で、一応試してみました。

現状、Arduino IDEには二種類あります。一つはLegacy IDE(Arduino IDE 1)と呼ばれるもの。

Legacy IDE
Legacy IDE (引用:https://docs.arduino.cc/software/ide-v1/tutorials/Environment/)

もう一つは、Arduino IDE 2と呼ばれるもの。

Arduino IDE 2
Arduino IDE 2 (引用:https://docs.arduino.cc/software/ide-v2/tutorials/getting-started-ide-v2/)

Legacy IDE

Legacy版と呼ばせてもらいますが、こちらは永らく愛用する方が多い印象です。一方私は新参者故か、一年ほど存在すら知らずに居りました。

Legacy版に関する情報は幾つか見つかります。手動でインストールする方法もあるようですが、コマンド一つでも済むようです。

sudo apt install arduino

ただ、「ボード選択」をしていない状態では、シリアルモニターを開けないようでした。

ボードnullは利用できません

と出てきます。本来はボードを接続して使うものですから、接続せずにエラーが出るのは真っ当です。今はArduino開発が目的というわけではありませんので、これ以上は試しておりません。

Arduino IDE 2

Raspberry Piに限らず、こちらを使用する記事は少ない印象です。その例に漏れず、こちらをインストールしている記事は殆ど見つかりません。

一つは、Raspberry Piに向けたものではないものの、公式の情報があります。

https://docs.arduino.cc/software/ide-v2/tutorials/getting-started/ide-v2-downloading-and-installing/

Ubuntuの場合の手順があったので試しましたが、私はうまくいきませんでした。

もう一つ、物々しい記事があります。私が調べた限りでは、唯一Raspberry Piへインストールできている(ように見える)情報になります。

https://github.com/koendv/arduino-ide-raspberrypi/

上の情報に似た手順も示していますが、私はこちらも駄目でした。しかし、様子の異なる情報も含まれています。要するに、「提供されていないから自分で用意する」という話のようです。見ると分かる通り手順が煩雑ですから、シリアルモニターを使いたいがために征くべき道ではありません。

VSCode拡張機能 Serial Monitor

VSCodeにも、シリアルモニターの拡張機能が存在します。冒頭にも末尾にもある通り、これを使いましょう。

https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-serial-monitor

Raspberry Piでは、VSCodeのインストールを次のコマンド一つで行います。

sudo apt install code

インストールすると、メニューに追加されます。

メニューからVSCodeを開く
メニュー→プログラミングの中に追加されている

日本語化も拡張機能のインストールで行います。

https://marketplace.visualstudio.com/items?itemName=MS-CEINTL.vscode-language-pack-ja

拡張機能のインストール

拡張機能のアイコン
元が暗色なので明るく補正

画像のアイコンから、拡張機能のメニューが現れます。

拡張機能のメニュー

検索窓でserial monitorを探して、インストールします。

Serial Monitorを検索する

画面下にマウスポインターを遣ると、に変化する箇所があります。その箇所から上に引っ張ると、コンソールの類が出てきます。

下から引っ張る
下部にマウスポインターを合わせ、になる所で引っ張り上げる

この中にSERIAL MONITORがあります。分かりにくいので、私はドラッグ&ドロップで左のメニューに移しています。これは好みです。

移動の様子

掴む
掴むと浮かび上がる

左のメニューまで移動する
左のメニューに移動すると、白い線が出る

アイコンができる
アイコンができて分かりやすい

どこにあるか分かりやすい一方で、縦長なので普通に使いづらいです。私は甘受しています。

動作確認

行の終わりをLFにする
なんとなくLFにしておく

Tera Term同様、改行コードの種類を選択できます。ここでは無難にLFとしていますが、この選定は、対象とする周辺機器の仕様に従います。データシートを読みましょう。

英名 エスケープシーケンス 備考
LF
Line Feed
\n Linuxや今のMacなどで使われる
CR
Carriage Return
\r 曽てのMacなどで使われる
CRLF \r\n DOS系、Windowsなどで使われる

監視の開始をクリックすると、シリアルモニターによる監視ができるようになります。

監視の開始

下の窓に入力した内容を送信します。表示がおかしく見づらいですが、serialと入力しています。

メッセージを送信する

送信した内容と、受信した内容が表示されます。

送信結果と受信内容

監視の停止で、シリアルモニターでの監視を閉じます。

シリアルモニターを自作する

最後に、趣味レベルのお話を付け加えます。案の定と言いましょうか、世の中には、シリアルモニターを自作している方々が既にいらっしゃるのです。

https://ganbaranai.tech/tech-blog/python-pySimpleGUI-SerialMonitor/

https://771-8bit.com/blog/pyserial/

私も当初、PythonTera Termのようなものを作ればよいのではないかと考えたものです。しかし一丁前に考えたはよいものの、当時は理解が乏しい上に余裕もなかったため、REPLで頑張った記憶があります。

今は生成AIも発達の途上にあるので、どうでもいいプログラムは作らせてしまうという選択肢があります。せっかくなので、当時の雪辱を果たすべく、AIにシリアルモニターを作らせてみました。誰もいらないと思うのでGitHubへは載せず置きます。

また、上二つの参考記事が使用するpysimpleguiですが、ライセンス形態を改めたことで一時騒がれました。商用利用について有償化しており、個人利用でもライセンス登録を要するとのことです。

https://news.mynavi.jp/techplus/article/zeropython-115/

ところで今回私が作らせたものは、tkinterのみで構成しています。甚だ傲慢と存じますが、一応代替となるでしょう。また、若し「VSCodeを導入できない」という場面がありましたら、こちらが使えるかもしれません。

source code

次の箇所は、python3.12以降の表記です。

type alias 3.12
type SerialConnectionState = Serial | None

以前のバージョン(3.11など)ではエラーになるので、typeを消します。

type alias 3.11
SerialConnectionState = Serial | None

以下は、後者に合わせました。


SerialApp.py
import threading
from tkinter import END, WORD, PhotoImage, Tk, Toplevel, messagebox, scrolledtext, ttk
from typing import Callable, Final
from serial.tools.list_ports_common import ListPortInfo
from serial import Serial, SerialException
from serial.tools import list_ports

# 接続状態型
# type SerialConnectionState = Serial | None
SerialConnectionState = Serial | None

class SerialApp:
    ''' シリアルモニター '''
    def __init__(self):
        ''' GUIの構成とUARTの設定 '''
        # initialize tk GUI
        self.__root: Final[Tk] = Tk()
        self.__root.title(
            string = "UART Serial Monitor"
        )
        self.__root.iconphoto(
            False,
            PhotoImage(
                file = 'src/icon.png'
            )
        )

        # スレッド停止フラグ
        self.__disconnect_flag = threading.Event()

        # シリアル設定データ
        self.__serial_settings = {
            "port":     None,
            "baudrate": 115200
        }
        # 接続状態
        self.__serial_connection: SerialConnectionState = None

        # 設定画面を表示する
        self.__open_settings_window()

        # メインウィンドウのウィジェットを作成
        self.__create_main_widgets()

    def __del__(self):
        if self.__serial_connection:
            self.__serial_connection.close()

    def mainloop(self):
        ''' メインウィンドウを起動する '''
        self.__root.mainloop()

    def __create_main_widgets(self):
        ''' メインウィンドウの画面構成 '''
        # 設定ボタン
        self.__settings_button = ttk.Button(
            master      = self.__root,
            text        = "設定",
            command     = self.__open_settings_window
        )
        self.__settings_button.grid(
            row         = 0,
            column      = 0,
            padx        = 5,
            pady        = 5
        )

        # 接続/切断ボタン
        self.__connect_button = ttk.Button(
            master      = self.__root,
            text        = "接続",
            command     = self.__toggle_connection
        )
        self.__connect_button.grid(
            row         = 0,
            column      = 1,
            padx        = 5,
            pady        = 5
        )

        # 送信テキストボックス
        self.__transmitt_label = ttk.Label(
            master      = self.__root,
            text        = "送信データ:"
        )
        self.__transmitt_label.grid(
            row         = 1,
            column      = 0,
            padx        = 5,
            pady        = 5,
            sticky      = "w"
        )
        self.__send_entry = ttk.Entry(
            master      = self.__root
        )
        self.__send_entry.grid(
            row         = 1,
            column      = 1,
            columnspan  = 3,
            padx        = 5,
            pady        = 5,
            sticky      = "we"
        )

        # 送信ボタン
        self.__send_button = ttk.Button(
            master      = self.__root,
            text        = "送信",
            command     = self.__send_data
        )
        self.__send_button.grid(
            row         = 1,
            column      = 4,
            padx        = 5,
            pady        = 5
        )

        # ログ表示エリア
        self.__log_area = scrolledtext.ScrolledText(
            master      = self.__root,
            wrap        = WORD,
            state       = "disabled",
            height      = 15
        )
        self.__log_area.grid(
            row         = 2,
            column      = 0,
            columnspan  = 5,
            padx        = 5,
            pady        = 5,
            sticky      = "nsew"
        )

        # レイアウト調整
        self.__root.grid_columnconfigure(
            index       = 1,
            weight      = 1
        )
        self.__root.grid_rowconfigure(
            index       = 2,
            weight      = 1
        )

    def __open_settings_window(self):
        """ ポート設定ウィンドウを開く """
        PortSettingsWindow(
            master                = self.__root,
            on_save_callback    = self.__save_settings
        )

    def __save_settings(self, port: ListPortInfo, baudrate: int):
        """ 設定を保存 """
        # 状態保存
        self.__serial_settings["port"]        = port
        self.__serial_settings["baudrate"]    = baudrate

        # 保存メッセージウィンドウ
        messagebox.showinfo(
            title   = "ポート設定",
            message = f"ポート: {port}\nボーレート: {baudrate}bps が保存されました。"
        )

    def __toggle_connection(self):
        ''' 接続状態切り替え '''
        if self.__serial_connection:
            self.__disconnect()
        else:
            self.__connect()

    def __connect(self):
        ''' 接続 '''
        # 切断中のみ実行する
        if not self.__serial_connection:
            # ポート設定篩
            if not self.__serial_settings["port"]:
                messagebox.showerror("エラー", "設定でポートを選択してください。")
                return

            try:
                # 通信設定
                self.__serial_connection: SerialConnectionState = Serial(
                    port        = self.__serial_settings["port"],
                    baudrate    = self.__serial_settings["baudrate"],
                    timeout     = 1
                )
                # ボタンを接続から切断へ
                self.__connect_button.config(
                    text        = "切断"
                )
                # 接続メッセージ
                self.__log_message(
                    message     = f"接続しました: {self.__serial_settings['port']} @ {self.__serial_settings['baudrate']}bps"
                )

                # 接続状態 (disconnect: False)
                self.__disconnect_flag.clear()
                # シリアル入力タスク
                self.__read_thread = threading.Thread(
                    target      = self.__read_data,
                    daemon      = True
                )
                self.__read_thread.start()
            except SerialException as e:
                messagebox.showerror(
                    title       = "エラー",
                    message     = f"接続できません: {e}"
                )

    def __disconnect(self):
        ''' 切断 '''
        # 接続中のみ実行する
        if self.__serial_connection:
            # スレッド停止
            self.__disconnect_flag.set()
            # スレッドが完全に終了するのを待つ
            self.__read_thread.join()

            # シリアル接続を閉じる
            self.__serial_connection.close()
            # 接続状態を変更する
            self.__serial_connection: SerialConnectionState = None
            # ボタンを切断から接続へ
            self.__connect_button.config(
                text = "接続"
            )
            # 切断メッセージ
            self.__log_message("切断しました。")

    def __send_data(self):
        ''' シリアル出力 '''
        # 接続中のみ実行する
        if self.__serial_connection:
            # 入力窓内容の取得
            data: str = self.__send_entry.get()
            if data:
                # シリアル出力
                self.__serial_connection.write(
                    data.encode()
                )
                # 送信メッセージ
                self.__log_message(
                    message = f"送信: {data}"
                )
        else:
            messagebox.showwarning(
                title   = "警告",
                message = "接続されていません。"
            )

    def __read_data(self):
        ''' シリアル入力 '''
        # 接続中のみ実行する
        if self.__serial_connection:
            while not self.__disconnect_flag.is_set():
                try:
                    # シリアル入力を読み取る
                    if data := self.__serial_connection.readline().decode().strip():
                        # 受信メッセージ
                        self.__log_message(
                            message = f"受信: {data}"
                        )
                except Exception as e:
                    self.__log_message(
                        message     = f"受信エラー: {e}"
                    )
                # 5ms待機
                self.__disconnect_flag.wait(0.005)

    def __log_message(self, message: str):
        ''' メッセージエリアへの表示 '''
        # 可変
        self.__log_area.config(
            state   = "normal"
        )
        # 表示
        self.__log_area.insert(
            index   = END,
            chars   = f'{message}\n'
        )
        # 不変
        self.__log_area.config(
            state   = "disabled"
        )
        # 可視
        self.__log_area.see(
            index   = END
        )

class PortSettingsWindow:
    ''' 設定画面 '''
    def __init__(self, master: Tk, on_save_callback: Callable[[SerialApp, ListPortInfo, int], None]):
        ''' 設定と画面構成 '''
        # 主体
        self.master: Tk = master
        # 設定保存時に呼び出すコールバック
        self.on_save_callback = on_save_callback
        # 画面構成
        self.create_setting_widgets()

    def create_setting_widgets(self):
        ''' 画面構成と有効ポートの取得 '''
        # ウィンドウの設定
        self.window = Toplevel(
            master      = self.master
        )
        self.window.title(
            string      = "ポート設定"
        )
        self.window.resizable(
            width       = False,
            height      = False
        )
        self.window.attributes(
            "-topmost",
            True
        )
        self.window.iconphoto(
            False,
            PhotoImage(
                file = 'src/icon.png'
            )
        )

        # ポート選択ラベルとコンボボックス
        self.port_label = ttk.Label(
            master      = self.window,
            text        = "ポート:"
        )
        self.port_label.grid(
            row         = 0,
            column      = 0,
            padx        = 10,
            pady        = 10,
            sticky      = "w"
        )
        self.port_combobox = ttk.Combobox(
            master      = self.window,
            state       = "readonly"
        )
        self.port_combobox.grid(
            row         = 0,
            column      = 1,
            padx        = 10,
            pady        = 10
        )
        self.update_ports()  # ポート一覧を更新

        # ボーレート選択ラベルとコンボボックス
        self.baudrate_label = ttk.Label(
            master      = self.window,
            text        = "ボーレート:"
        )
        self.baudrate_label.grid(
            row         = 1,
            column      = 0,
            padx        = 10,
            pady        = 10,
            sticky      = "w"
        )
        self.baudrate_combobox = ttk.Combobox(
            master      = self.window,
            state       = "readonly"
        )
        self.baudrate_combobox["values"] = [9600, 19200, 38400, 57600, 115200]
        self.baudrate_combobox.current(
            newindex    = 4
        )
        self.baudrate_combobox.grid(
            row         = 1,
            column      = 1,
            padx        = 10,
            pady        = 10
        )

        # 保存ボタン
        save_button = ttk.Button(
            master      = self.window,
            text        = "保存",
            command     = self.save_settings
        )
        save_button.grid(
            row         = 2,
            column      = 0,
            columnspan  = 2,
            pady        = 20
        )

    def update_ports(self):
        """ シリアルポートの一覧を取得してコンボボックスに設定 """
        # 有効シリアルポート一覧
        ports = list_ports.comports(True)
        port_names = [port.device for port in ports]
        # コンボボックスを更新する
        self.port_combobox["values"] = port_names
        # 有効ポートがある場合
        if port_names:
            # 最初のポートを選択状態にする
            self.port_combobox.current(0)
        # 無い場合
        else:
            # 空のリストを設定
            self.port_combobox["values"] = []
            # 表示をクリア
            self.port_combobox.set("")
            messagebox.showwarning(
                title   = "警告:シリアルポート無効",
                message = "利用可能なシリアルポートが見つかりません。",
                parent  = self.window
            )

    def save_settings(self):
        """ 設定を保存し、コールバックを呼び出す """
        # 選択されたものを取得する
        selected_port = self.port_combobox.get()
        selected_baudrate = self.baudrate_combobox.get()

        if not selected_port:
            messagebox.showerror(
                title   = "エラー",
                message = "ポートを選択してください。",
                parent  = self.window
            )
            return

        # 設定を保存するコールバックを呼び出す
        self.on_save_callback(selected_port, int(selected_baudrate))
        # 設定画面を閉じる
        self.window.destroy()


main.py
from SerialApp import SerialApp

SerialApp().mainloop()


icon.png
icon.png


実行の際はmain.pyを指定します。

python main.py

三つのファイルは全て同じフォルダー内に置きます。一応、構造に示しておきます。

$ tree
.
├── SerialApp.py
├── icon.png
└── main.py

1 directory, 3 files
実行の様子

設定画面
設定画面(左)でシリアルポートを選ぶ

設定確認
設定内容が表示される

接続
接続する

送信
送信(手動)と受信(自動)

畢竟

そもそも本記事で何を書こうとしていたのかというと、「Tera Termに代わるシリアルモニターとは」というのが主題でありました。しかしRaspberry Piの特性上、シリアルポートの設定は避けることを得ず、本題に至るまで随分掛かりました。

結論を明らかにすると、「VSCodeの拡張機能を使う」ことが最も簡単で、最も確実だろうと思われます。これからRaspberry PiUART通信を行われる方には、是非私の屍を越えて征いて頂きたく申し上げると共に、竟に聿を置きます。

Discussion