tkinterで簡易シリアル通信アプリ


# つくったもの
Aruduinoと通信してる様子（Arduinoは送信データをそっくりそのまま返すだけ）
![スクリーンショット 2023-04-02 12.37.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2839352/7d9f6a5e-1456-1316-fea1-8763224c78c4.png)


- シリアルポートの接続切断
- ボーレート、デリミタ設定
- 文字列の送受信

という機能だけのシンプルなアプリです。
    
# 準備
pythonで簡単にGUIアプリを作成できるTkinterと、
シリアル通信を可能にするpyserialをインストールします。
~~~python:
$ pip3 install python3-tk 
$ pip3 install pyserial
~~~


# コードと解説
~~~python:
import tkinter as tk
import tkinter.messagebox as messagebox
import serial.tools.list_ports
from serial import Serial
import threading
import datetime

# ボーレートのリスト
BAUDRATES = [9600, 19200, 38400, 115200]
# デリミタのリスト
DELIMITERS = {'[CR]': '\r', '[LF]': '\n', '[CRLF]': '\r\n', '[None]': ''}

# デリミタのリスト(履歴表示用)
REV_DELIMITER = { '\r':'[CR]',  '\n':'[LF]', '\r\n':'[CRLF]', '':''}

#シリアルポート情報の取得
def refresh_serial_ports():
    global port_var
    ports = serial.tools.list_ports.comports()
    port_names = [port.device for port in ports]
    port_var.set('')  # 初期値を空に設定
    port_menu["menu"].delete(0, tk.END)
    for port_name in port_names:
        port_menu["menu"].add_command(label=port_name, command=tk._setit(port_var, port_name))

#シリアルポートの接続・切断
def toggle_serial_port():
    global ser
    global read_thread #受信用スレッド

    if ser.is_open:
        try:
            ser.close()
            baudrate_menu.config(state="normal") 
            delimiter_menu.config(state="normal")
            toggle_button.config(text="接続")
            messagebox.showinfo("情報", "シリアルポートが切断されました")
        except Exception as e:
            messagebox.showerror("切断エラー", str(e))
    else:
        port = port_var.get()
        if not port:
            messagebox.showerror("接続エラー", "シリアルポートが選択されていません")
            return
        baudrate = int(baudrate_var.get())
        try:
            ser = Serial(port, baudrate)       
            #ポート接続中はボーレート・デリミタを変更できないように
            baudrate_menu.config(state="disabled") 
            delimiter_menu.config(state="disabled")
            messagebox.showinfo("情報", "シリアルポートに接続しました")
            # 受信スレッドを開始
            read_thread = threading.Thread(target=read_data, daemon=True)
            read_thread.start()
            toggle_button.config(text="切断")
        except Exception as e:
            messagebox.showerror("受信エラー", str(e))

#データ送信
def send_data():
    #送信ボックスの入力値を取得
    data = entry_text.get()
    #デリミタを取得
    delimiter = DELIMITERS[delimiter_var.get()]
    try:
        #送信
        ser.write((data + delimiter).encode())
        #履歴フィールドに時刻とともに表示
        timestamp = datetime.datetime.now().strftime('%H:%M:%S.%f')
        received_text.insert(tk.END, f"[Send -> {timestamp}] {data}{REV_DELIMITER[delimiter]}\n")
        received_text.see("end")    #自動スクロール
        
    except Exception as e:
        messagebox.showerror("送信エラー", str(e))

#データ受信
def read_data():
    #設定したデリミタを取得
    delimiter = DELIMITERS[delimiter_var.get()]
    buf = ''
    while ser.is_open:
        try:
            #一文字ずつ読み出す
            data = ser.read(1).decode()
            if data:
                buf += data
                #デリミタを含む場合
                if delimiter == '':
                    timestamp = datetime.datetime.now().strftime('%H:%M:%S.%f')
                    received_text.insert(tk.END, f"[Recv <- {timestamp}] {buf}\n")
                    buf = ''    #受信バッファをクリア
                elif delimiter in buf:
                    #表示用にデリミタを変換（'\r'を'[CR]'に）
                    buf = buf.replace(delimiter, REV_DELIMITER[delimiter])
                    #履歴フィールドにタイムスタンプと共に表示
                    timestamp = datetime.datetime.now().strftime('%H:%M:%S.%f')
                    received_text.insert(tk.END, f"[Recv <- {timestamp}] {buf}\n")
                    buf = ''    #受信バッファをクリア

        except Exception as e:
            messagebox.showerror("受信エラー", str(e))

#履歴をクリア
def clear_received_text():
    received_text.delete(1.0, tk.END)

# GUIのルートを作成
root = tk.Tk()
root.title("tkinterでシリアル通信")
root.geometry("500x400")

# 上部フレーム
frame_top = tk.Frame(root)
frame_top.pack(pady=5)


# シリアルポート選択メニュー
port_var = tk.StringVar(root)
port_menu = tk.OptionMenu(frame_top, port_var, '')
port_menu.pack(side=tk.LEFT, padx=5)

# シリアルポートを更新
refresh_serial_ports()

# 接続/切断ボタン
ser = Serial()
ser.close()
toggle_button = tk.Button(frame_top, text="接続", command=toggle_serial_port)
toggle_button.pack(side=tk.LEFT, padx=5)

# ボーレート選択メニュー
baudrate_var = tk.StringVar(root)
baudrate_var.set(BAUDRATES[0])
baudrate_menu = tk.OptionMenu(frame_top, baudrate_var, *BAUDRATES)
baudrate_menu.pack(side=tk.LEFT, padx=5)

# デリミタ選択メニュー
delimiter_var = tk.StringVar(root)
delimiter_var.set(REV_DELIMITER['\r'])
delimiter_menu = tk.OptionMenu(frame_top, delimiter_var, *DELIMITERS.keys())
delimiter_menu.pack(side=tk.LEFT, padx=5)

# 送信文字列入力ボックス
entry_text = tk.Entry(root)
entry_text.pack(fill=tk.X, padx=5, pady=5)

# 送信ボタン
send_button = tk.Button(root, text="送信", command=send_data)
send_button.pack(pady=5)

# 受信文字列を表示するエリア
received_text = tk.Text(root, height=15)
received_text.pack(fill=tk.BOTH, padx=5, pady=5)

# クリアボタン
clear_button = tk.Button(root, text="履歴クリア", command=clear_received_text)
clear_button.pack(pady=5)

root.mainloop()
~~~

# ポイント
- 受信処理は別スレッドでポーリングして行っている
- 受信でもデリミタを見ているので、受信データのデリミタが合わない場合は履歴に表示されない
- デリミタは普通に履歴に表示すると改行になってしまうので表示用に変換している

適当にカスタマイズして使ってみてください