PR

Tkinter(Python)でXYテーブルの操作GUI(グラフィカル)を作る【XYテーブル自作 第8回】

RaspberryPi
スポンサーリンク

はじめに

XYテーブル製作シリーズ 第8回

スポンサーリンク

はじめに:ウィンドウ(アプリ)からXYテーブルを操作する

前回まではPythonプログラムからXYテーブルを自動で動かしていました。実際の装置では「移動量を変えたい」「途中で止めたい」「手動操作したい」といった操作が必要です。今回は Python 標準GUIライブラリ tkinter操作ソフト(アプリ)を作ります。

この記事で分かること

  • Tkinter の基本要素(Label・Entry・Button)と grid() 配置
  • Entry から移動量・サイクル数を float() / int() で取り出してモーター関数へ渡す
  • 「XY同時実行」ボタンで threading により2軸を同時起動する(GUIをフリーズさせない)
  • フラグ方式の「緊急停止」ボタンと、A4988の電流OFF(EN=HIGH
  • フルコード(XY_gui.py、ZIP配布)

完成する操作画面:X移動量(mm)/Xサイクル/Y移動量(mm)/Yサイクル の入力欄+「XY同時実行」「緊急停止」ボタン


スポンサーリンク

使用環境とGPIOピン設定

項目内容
マイコンRaspberry Pi
モータードライバ ×2A4988
モーター ×2NEMA17
電源12V 5A
Python3.x(tkinter は標準搭載)
# X軸  X_STEP=21 X_DIR=20 X_EN=16 X_MS1=17 X_MS2=27 X_MS3=22 X_RESET=5 X_SLEEP=6 X_LIMIT=4
# Y軸  Y_STEP=2  Y_DIR=26 Y_EN=18 Y_MS1=23 Y_MS2=24 Y_MS3=25 Y_RESET=12 Y_SLEEP=13 Y_LIMIT=14
A4988ドライバー2台・リミットスイッチ・Raspberry Piの結線図
第7回と同じ結線

スポンサーリンク

Tkinterの基本と各機能

Tkinterの基本要素

部品用途
Label文字表示
Entry入力欄
Buttonボタン
root = tk.Tk()
tk.Label(root, text="X移動量").grid(row=0, column=0)
entry = tk.Entry(root)
entry.grid(row=0, column=1)
root.mainloop()

移動量の入力

distance = float(entry_x.get())   # 入力欄の値を数値化してモーター関数へ渡す

XY同時制御(threading)

XとYを同時に動かすには threading.Thread を使います。GUIメインスレッドでモーター処理を直接回すとウィンドウが固まるためです(詳しくは第8.5回)。

tx = threading.Thread(target=run_motor, args=(...))
ty = threading.Thread(target=run_motor, args=(...))
tx.start()
ty.start()

緊急停止(フラグ方式)

# ボタン側
stop_flag = True
# モーター側(ループ内で毎回チェック)
if stop_flag:
    return
# さらにA4988の電流をOFF
GPIO.output(X_EN, GPIO.HIGH)

スポンサーリンク

第8回 完成コード

# ============================================
# 第8回
# XYテーブルGUI操作
# ============================================

import RPi.GPIO as GPIO
import time
import threading
import tkinter as tk

GPIO.setmode(GPIO.BCM)

# ============================================
# GPIO設定
# ============================================

X_STEP = 21
X_DIR  = 20
X_EN   = 16

X_MS1 = 17
X_MS2 = 27
X_MS3 = 22

X_RESET = 5
X_SLEEP = 6

X_LIMIT = 4


Y_STEP = 2
Y_DIR  = 26
Y_EN   = 18

Y_MS1 = 23
Y_MS2 = 24
Y_MS3 = 25

Y_RESET = 12
Y_SLEEP = 13

Y_LIMIT = 14


# ============================================
# モーション設定
# ============================================

BELT_PITCH = 2.0
PULLEY_TEETH = 32

MM_PER_REV = BELT_PITCH * PULLEY_TEETH

FULL_STEPS = 200
MICROSTEP = 16

STEPS_PER_REV = FULL_STEPS * MICROSTEP
STEPS_PER_MM = STEPS_PER_REV / MM_PER_REV

ACC_DEC_STEPS = 100

stop_flag = False


# ============================================
# GPIO初期化
# ============================================

def gpio_init_axis(STEP,DIR,EN,MS1,MS2,MS3,RESET,SLEEP):

    pins=[STEP,DIR,EN,MS1,MS2,MS3,RESET,SLEEP]

    for p in pins:
        GPIO.setup(p,GPIO.OUT)

    GPIO.output(RESET,GPIO.HIGH)
    GPIO.output(SLEEP,GPIO.HIGH)

    GPIO.output(MS1,GPIO.HIGH)
    GPIO.output(MS2,GPIO.HIGH)
    GPIO.output(MS3,GPIO.HIGH)

    GPIO.output(EN,GPIO.LOW)


# ============================================
# 加減速
# ============================================

def get_intervals(total_steps,acc_steps,total_time):

    const_steps=max(total_steps-acc_steps*2,1)

    total_ratio=acc_steps*0.5+const_steps+acc_steps*0.5

    base_interval=total_time/total_ratio/2

    intervals=[]

    for i in range(acc_steps):
        intervals.append(base_interval*(acc_steps-i)/acc_steps)

    for i in range(const_steps):
        intervals.append(base_interval)

    for i in range(acc_steps):
        intervals.append(base_interval*(i+1)/acc_steps)

    return intervals


# ============================================
# モーター制御
# ============================================

def run_motor(axis_name,
              STEP,DIR,EN,
              MS1,MS2,MS3,
              RESET,SLEEP,
              distance_mm,
              cycles):

    global stop_flag

    gpio_init_axis(STEP,DIR,EN,MS1,MS2,MS3,RESET,SLEEP)

    steps=int(distance_mm*STEPS_PER_MM)

    intervals=get_intervals(steps,ACC_DEC_STEPS,1.5)

    for c in range(cycles):

        if stop_flag:
            return

        if axis_name=="X軸":
            GPIO.output(DIR,GPIO.LOW)
        else:
            GPIO.output(DIR,GPIO.HIGH)

        for t in intervals:

            if stop_flag:
                return

            GPIO.output(STEP,GPIO.HIGH)
            time.sleep(t)

            GPIO.output(STEP,GPIO.LOW)
            time.sleep(t)

    GPIO.output(EN,GPIO.HIGH)


# ============================================
# XY同時実行
# ============================================

def start_xy():

    x_dist=float(entry_x_dist.get())
    x_cycle=int(entry_x_cycle.get())

    y_dist=float(entry_y_dist.get())
    y_cycle=int(entry_y_cycle.get())

    tx=threading.Thread(
        target=run_motor,
        args=("X軸",
              X_STEP,X_DIR,X_EN,
              X_MS1,X_MS2,X_MS3,
              X_RESET,X_SLEEP,
              x_dist,x_cycle)
    )

    ty=threading.Thread(
        target=run_motor,
        args=("Y軸",
              Y_STEP,Y_DIR,Y_EN,
              Y_MS1,Y_MS2,Y_MS3,
              Y_RESET,Y_SLEEP,
              y_dist,y_cycle)
    )

    tx.start()
    ty.start()


# ============================================
# 緊急停止
# ============================================

def stop_motor():

    global stop_flag
    stop_flag=True

    GPIO.output(X_EN,GPIO.HIGH)
    GPIO.output(Y_EN,GPIO.HIGH)


# ============================================
# GUI
# ============================================

root=tk.Tk()
root.title("XY Table Controller")


tk.Label(root,text="X移動量(mm)").grid(row=0,column=0)
entry_x_dist=tk.Entry(root)
entry_x_dist.grid(row=0,column=1)

tk.Label(root,text="Xサイクル").grid(row=1,column=0)
entry_x_cycle=tk.Entry(root)
entry_x_cycle.grid(row=1,column=1)


tk.Label(root,text="Y移動量(mm)").grid(row=2,column=0)
entry_y_dist=tk.Entry(root)
entry_y_dist.grid(row=2,column=1)

tk.Label(root,text="Yサイクル").grid(row=3,column=0)
entry_y_cycle=tk.Entry(root)
entry_y_cycle.grid(row=3,column=1)


tk.Button(root,
          text="XY同時実行",
          command=start_xy,
          bg="lightblue").grid(row=4,column=0,columnspan=2,pady=10)


tk.Button(root,
          text="緊急停止",
          command=stop_motor,
          bg="red",
          fg="white").grid(row=5,column=0,columnspan=2,pady=10)


root.mainloop()
⬇ XY_gui.py をダウンロード(ZIP)

動作手順

  1. Raspberry Pi で python xy_gui.py を実行
  2. GUIが起動する
  3. 移動量・サイクル数を入力する
  4. 「XY同時実行」で動作、「緊急停止」で停止
Raspberry Piで起動したXYテーブル操作GUIのウィンドウ
起動したXYテーブル操作GUI

スポンサーリンク

まとめと次回予告

Pythonだけで「移動量入力・XY同時制御・緊急停止」を備えた装置操作ソフトの基礎が完成しました。ただし「GUIが固まる」問題を正しく理解しておく必要があります。次回・第8.5回でThreadの使い方とGUIが固まる理由を掘り下げ、第9回で原点復帰GUI・現在位置表示・ソフトリミットを追加します。

次に読む記事

🧵 次回はこちら(第8.5回) PythonのGUIが固まる理由とThreadの使い方|Tkinterでモーター制御中もボタンを効かせる なぜThreadが必要なのかを図解。 🗺️ 全体像 自作XYテーブルの全手順ロードマップ|Raspberry Pi+ステッピングモーターで2軸制御 第0回〜第11回の全工程をまとめたロードマップ。

スポンサーリンク

よくある質問(FAQ)

Qtkinter がインポートできません(No module named ‘tkinter’)。

ARaspberry Pi OS では通常標準搭載ですが、無い場合は sudo apt install python3-tk でインストールします。仮想環境を使っている場合はシステムのtkが見えないことがあるので、–system-site-packages で環境を作り直すか、システムのpython3で実行します。

Qボタンを押すとGUIが固まって動かなくなります。

Acommand に指定した関数の中でモーター処理(forループ+time.sleep)を直接実行しているためです。threading.Thread でモーター処理を別スレッドに逃がすと、GUIメインスレッドはイベント処理を続けられます。第8.5回で詳しく解説します。

Q緊急停止ボタンを押しても止まりません。

Aモーターのforループ内で毎ステップ if stop_flag: return をチェックしているか確認します。ループの外にチェックがあると、1回の移動が終わるまで止まりません。あわせて GPIO.output(EN, GPIO.HIGH) でドライバの電流も切ります。

QEntry が空のまま実行するとエラーで落ちます。

Afloat(entry.get()) は空文字だと ValueError になります。try/except ValueError で囲んでメッセージを出すか、入力チェック(未入力なら実行しない)を関数の先頭に入れます。

QGUIの見た目が古い/ボタンを大きくしたい。

Atkinter は素朴な見た目です。ttk(tkinter.ttk)のウィジェットを使うとOSネイティブ風になります。ボタンサイズは width / height / padx / pady、色は bg / fg で調整します。凝ったUIが必要なら CustomTkinter も選択肢です。


スポンサーリンク

XYテーブル製作シリーズ(全12回)

補足記事

第1.5回・補足 A4988の電流制限(Vref)調整方法|焼損を防ぐ計算式とテスターでの設定手順 モーターを回す前に必ずやる A4988 の電流調整。 🔀 番外編 L6470が動かない原因|A4988との違いとSPI通信・デイジーチェーンの罠 高機能ドライバ L6470 を A4988 と同じ感覚で使ってハマった話。 🗺️ 全体像 自作XYテーブルの全手順ロードマップ|Raspberry Pi+ステッピングモーターで2軸制御 第0回〜第11回の全工程をまとめたロードマップ。

ニンジン🥕アイコン

Follow me on X

本ブログの中の人「ニンジン🥕」はXやってます。新着記事はいち早くポストでお知らせしているので、見逃したくない方は @ninjin_py_vba をフォローしてみてください🥕

@ninjin_py_vba をフォロー

この記事が役に立ったら、シェアしてもらえると嬉しいです🥕

Xでポスト はてブ LINE Pocket

PROJECT LOG — SERIES INDEX
スポンサーリンク

シリーズ記事一覧

電子工作・自動化ツールの製作記録をシリーズごとにまとめています。気になるテーマからお読みください。


超音波距離センサーHC-SR04をRaspberry Pi Picoで使う|仕組み・配線・精度検証
.uds1-block { position: relative; overflow: hidden; border-radius: 12px; border: 1px solid #f0cba8; background: linear-g…
今週の注目ガジェット〜09/07週〜|電子工作・自作向け新製品まとめ(TickrCast/Radxa Linkr ほか)
ニンジンのガジェット通信 09/07週。ESP32-P4の自作気象モニター、極小IP-KVM「Radxa Linkr」、ESP32のDINレール型PLC、LTE開発キットなど、電子工作で使えそうな新製品5つを解説。
サーボモータの角度をタクトスイッチで可変|OLED(SSD1306)に現在角度をリアルタイム表示
Raspberry Pi Pico 2W×MG996RにタクトスイッチとOLED(SSD1306)を追加。ボタンで角度を可変しながら現在角度をリアルタイム表示するMicroPython実装を、framebufの文字拡大やサーボの遊び対策まで実機ベースで解説します。
MG996RサーボモーターをRaspberry Pi Pico 2Wで動かす:PWM制御とキャリブレーション実践
MG996RサーボモーターをRaspberry Pi Pico 2Wで動かす方法を解説。公称180度のはずが実測198度だった個体差の原因究明から、ホーンのズレ補正、タクトスイッチでの角度制御まで実機検証の様子を紹介します。
Tapo P110Mの消費電力をExcelレポート化する【年次集計・電気代試算・セキュリティ判断編】
FastAPI+openpyxlでTapo P110Mの消費電力をExcelレポート化。15秒間隔の生ログを日次集計する設計や、31円/kWhでの電気代試算、外部公開を見送った理由まで解説します。

コメント