3Dプリンターをpythonで制御してみた transitions編

前回

aka1022.hatenablog.com

緒言

この記事で3Dプリンターをシリアル通信経由でPC状のpythonから制御した。
制御といっても、エクストルーダを任意の方向へ動かすモードとレベリング用に事前設定した四隅にキー入力に応じて移動するレベリングモードの2モードに応じてエクストルーダの移動命令を送信するだけである。 備え付けの操作盤が非常に使いづらかったので、エクストルーダを頻繁に動かす際には有用だった。
しかし、エクストルーダ移動モードからレベリングモードへのモード切替は可能だが、その逆は不可能だったりと少しばかり不満があったので、改良した。 主な改良は

  1. transitionsを使った状態遷移モデル設計
  2. クラス化

の2点。改良といいつつ、設計から見直している。
クラス化については、通信系をPrinterDriverクラスとしてまとめ、キーボード入力からプリンターの制御量を決定する機能をクラスにまとめる。

transitionというのはpythonで状態遷移図を扱うためのモジュールである。
状態と遷移を定め遷移を呼び出すと、遷移に対応した処理を行い、状態を切り替えることができる。
今回はそこまで複雑な状態遷移を作成するつもりはなかったが、ほかにも応用が効きそうなモジュールであったため、練習がてら今回使用する。

作成する状態遷移図

以下の状態遷移図をtransitionに実装する。
ほとんど前回の記事と一緒だが、緒言で書いた通り、LevelingModeからMoveExtへの遷移が追加されている。
トリガー名が遷移の名前で処理が遷移時に実行される処理関数である。

状態遷移図

プログラム実行時の初期状態はEntranceであり、最初にStartStateを実行しMoveExtへ遷移する。その際に3Dプリンターとの通信のためのコムポートを開いたり、ホームポジションへの移動をしたりなどする。

以降の遷移は、キーボード入力を待ち、入力に対応する遷移を実行することを繰り返す。 Exitへの遷移時のExitProcでコムポートを閉じ、プログラムを終了する。

実装

以下が前章で示した状態遷移図を実装したコードである。

transitionsの具体的な使い方はググればわかりやすい日本語記事が出てくるのでそちらを参照してください。
簡単にはMachineクラスを持つクラス(ここではStateMachine)を用意し、コンストラクタを実行時に状態(states)を渡す。 その後にadd_transitionで遷移を定義する。その際にtriggerと遷移先元、遷移時の処理(before, after)を指定する。遷移時の処理はStateMachineのメソッド関数の名前を渡す。 遷移をするときは、trigger([トリガー名])で遷移できる。

from transitions import Machine
from msvcrt import getch #key input
import time
import serial #serial
from itertools import cycle 


class PrinterDriver:

    debug = True

    corner=cycle([
    [20, 20],
    [270, 20],
    [270, 270],
    [20, 270]
    ])

    # serial port
    COM = 'COM7'

    #gcode
    checktemp = b"M105"
    autohome = b"G28"
    moveX = b"G0 X "
    moveY = b"G0 Y "
    moveZ = b"G0 Z "
    setAbsolute = b"G90"
    setRelative = b"G91"

    comport = None

    def __init__(self, commpoart):
        self.COM = commpoart

    def openComport(self):
        if self.debug == False:
            self.comport = serial.Serial(self.COM, baudrate=115200, parity=serial.PARITY_NONE, timeout=0.1)
            #相手の再起動待ち
            time.sleep(5)
            #念のためバッファクリア
            self.comport.reset_input_buffer()

    def moveExtruder(self, dim, val):
        comm = str('')
        if dim == 'X':
            comm = self.moveX+str(val).encode()
            pass
        elif dim == 'Y':
            comm = self.moveY+str(val).encode()
            pass
        elif dim == 'Z':
            comm = self.moveZ+str(val).encode()
            pass
        else:
            pass
        self.sendcommand_waitless(comm)
        
    def sendcommand_waitless(self, command):
        if self.debug == False:
            self.comport.write(command)#commandを送る。commandはバイナリ文字列
            self.comport.write(b"\n")
            print("send: "+command.decode())
            time.sleep(0.5)
            #print("return: "+comport.readline().decode())#返答表示
            return self.comport.readline().decode()
        else:
            print("send: "+command.decode())
            return "recived message"

    #gcodeを送り、返信を表示
    #okが帰るまでwait秒待つ。
    #wait秒以内に戻らなければ、-1、そうでなければ、0を返す。
    def sendcommand_wait(self, command, wait):
        echo=0
        count=0
        if self.debug == False:
            self.comport.write(command)
            self.comport.write(b"\n")
            print("send: "+command.decode())
            while not ( (echo == b"ok\n") or (count >wait)):
                echo=self.comport.read(3)
                print("wait "+command.decode()+" "+str(count)+"/"+str(wait))
                time.sleep(1)
                count=count+1
            else:
                if echo == b"ok\n":
                    print("finished")
                    return 0
                else:
                    print("troubled")
                    return -1
        else:
            print("send: "+command.decode())
            while not count >wait:
                print("wait "+command.decode()+" "+str(count)+"/"+str(wait))
                time.sleep(1)
                count=count+1
            print("finished")
            return 0

    def closeComport(self):
        if self.debug == False:
            self.comport.close()
    pass


class StateMachine(object):
    # 状態遷移を管理するMachineクラスのインスタンス
    machine=None

    # PrinterDriverクラスのインスタンス
    Printer = None

    #状態の定義
    states = ['Entrance', 'MoveExt', 'Leveling', 'Exit']

    # キー入力バッファ(長押し対応予定)
    keyBuffer = 0

    """
    keyInput: キー入力に対する処理を記載する辞書
    トリガーをaddする際に追加する。
    {
        [ステート名]:{
            [キー番号]:{
                'trigger':[トリガー名],
                'Informaiton':[表示用の説明文]
            },...
        },...
    }
    """
    keyInput = {}

    # コンストラクタ
    def __init__(self, name):
        # 名前(使わないと思う)
        self.name = name
        # machineのコンストラクト
        self.machine = Machine(model=self, states=self.states, initial='Entrance', auto_transitions=False)
        # 初期処理実行用遷移の追加
        self.machine.add_transition(trigger='StartState', source='Entrance', dest='MoveExt', before= 'InitProc', after= ['enterMoveExt', 'PrintKeyInput'])

        # keyInput辞書の初期化
        for st in self.states:
            self.keyInput[st] = {}
        
        # 各モードでの遷移の追加
        self.setMoveExtTrans()
        self.setLevelingTrans()

    # 初期処理
    def InitProc(self):
        print('3DPrinter Initting ...')

        # プリンタードライバーの実体化
        self.Printer = PrinterDriver('COM7')
        # ポートオープン
        self.Printer.openComport()
        
        # ( 本当はエラー処理をするべき 例;コムポートがない )

        #print(self.state)
        #ホームポジションに移動
        self.Printer.sendcommand_wait(self.Printer.autohome,10)

    # エクストルーダ移動モードの遷移の追加
    def setMoveExtTrans(self):
        # ステート名の定義
        state = 'MoveExt'

        # 終了処理への遷移 Exit
        self.machine.add_transition(trigger='Exit', source=state,  dest='Exit', before= 'ExitProc')
        self.keyInput[state][27]={'trigger':'Exit', 'Information':'Exit Process'}

        # レベリングモードへの遷移 switchMode
        self.machine.add_transition(trigger='switchMode', source=state,  dest='Leveling', before= 'SwitchToLevel', after= 'PrintKeyInput')
        self.keyInput[state][ord('b')]={'trigger':'switchMode', 'Information':'change to Leveling Mode'}

        # キー入力処理用遷移 MoveProcess
        self.machine.add_transition(trigger='MoveProcess', source=state,  dest=state, before= 'MoveProc')
        self.keyInput[state][ord('a')]={'trigger':'MoveProcess', 'Information':'Move to X <'} # X <
        self.keyInput[state][ord('d')]={'trigger':'MoveProcess', 'Information':'Move to X >'} # X >
        self.keyInput[state][ord('w')]={'trigger':'MoveProcess', 'Information':'Move to Y ^'} # Y ^
        self.keyInput[state][ord('s')]={'trigger':'MoveProcess', 'Information':'Move to Y v'} # Y v
        self.keyInput[state][ord('q')]={'trigger':'MoveProcess', 'Information':'Move to Z ^'} # Z ^
        self.keyInput[state][ord('e')]={'trigger':'MoveProcess', 'Information':'Move to Z v'} # Z v
        self.keyInput[state][ord('h')]={'trigger':'MoveProcess', 'Information':'Move to home position'} # home position

    # エクストルーダ移動モードの初回処理
    def enterMoveExt(self):
        self.Printer.sendcommand_waitless(self.Printer.setRelative)
        print('set relateve')
        pass

    # エクストルーダ移動モードから抜ける時の処理    
    def escapeMoveExt(self):
        self.Printer.sendcommand_waitless(self.Printer.setAbsolute)
        print('set absolute')
        pass        

    # 移動時処理
    def MoveProc(self):
        key = self.keyBuffer
        if key == 97:#a 
            self.Printer.moveExtruder('X', -5)
            #sendcommand_waitless(moveX+b"-5")
        elif key ==100:#d ->
            self.Printer.moveExtruder('X',  5)
            #sendcommand_waitless(moveX+b"5")
        elif key ==119:#w 
            self.Printer.moveExtruder('Y',  5)
            #sendcommand_waitless(moveY+b"5")
        elif key ==115:#s
            self.Printer.moveExtruder('Y', -5)
            #sendcommand_waitless(moveY+b"-5")
        elif key ==113:#q 
            self.Printer.moveExtruder('Z',  5)
            #sendcommand_waitless(moveZ+b"5")
        elif key ==101:#e
            self.Printer.moveExtruder('Z', -5)
            #sendcommand_waitless(moveZ+b"-5")
        elif key ==ord('h'):#h
            self.Printer.sendcommand_wait(self.Printer.autohome,10)
                 
    # レベリングモードへの切り替え
    def SwitchToLevel(self):
        self.escapeMoveExt()
        print('Switch to Leveling Mode')
        self.enterLevelingMode()

    # レベリングモードの遷移の設定
    def setLevelingTrans(self):
        state = 'Leveling'
        self.machine.add_transition(trigger='Exit', source=state,  dest='Exit', before= 'ExitProc')
        self.keyInput[state][27]={'trigger':'Exit', 'Information':'Exit Process'}

        self.machine.add_transition(trigger='switchMode', source=state,  dest='MoveExt', before= 'SwitchToMoveExt', after= 'PrintKeyInput')
        self.keyInput[state][ord('b')]={'trigger':'switchMode', 'Information':'change to Move Extruder Mode'}

        self.machine.add_transition(trigger='TGoToCorner', source=state,  dest=state, before= 'GoToCorner')
        self.keyInput[state][32]={'trigger':'TGoToCorner', 'Information':'Go to Next Leveling Corner'}

    # レベリングモードの初期処理
    def enterLevelingMode(self):
        self.Printer.sendcommand_waitless(self.Printer.setAbsolute)
        print('set relateve')
        pass
    
    # レベリングモードの終了時処理
    def escapeLevelingMode(self):
        #self.Printer.sendcommand_waitless(self.Printer.setRelative)
        #print('set absolute')
        pass
    
    # 角位置の遷移
    def GoToCorner(self):
        if self.keyBuffer == ord(' '):
            cr = next(self.Printer.corner)
            print('Go to X '+str(cr[0])+' Y '+str(cr[1]))
            self.Printer.moveExtruder('Z', 5)
            self.Printer.moveExtruder('X', cr[0])
            self.Printer.moveExtruder('Y', cr[1])
            self.Printer.moveExtruder('Z', 0)
            pass
        pass

    # 移動モードの遷移
    def SwitchToMoveExt(self):
        print('Swtich to Exetoruder Move Mode')
        self.escapeLevelingMode()
        self.enterMoveExt()

    # キー入力表示
    def PrintKeyInput(self):
        print('')
        print('Now state : '+self.state)
        for k, v in self.keyInput[self.state].items():
            if k == 27:
                key = 'Esc'
            elif k == 32:
                key = 'space'
            else:
                key = str(chr(k))

            print('key '+key+' : '+v['Information'])
            pass

   # 終了時処理
    def ExitProc(self):
        print('Exit Prog')
        self.Printer.closeComport()
        exit()

    # メイン処理
    def mainProc(self):
        self.keyBuffer = ord(getch())
        #print(key)
        dict=self.keyInput[self.state].get(self.keyBuffer)
        #print(trig)
        if dict != None:
            #print(dict['trigger'])
            self.trigger(dict['trigger'])
        else:
            print('key input '+str(self.keyBuffer)+' is invalid')


# 状態遷移図の実体化
STM = StateMachine('buzz')

# 初期処理の実行
STM.trigger('StartState')

while True:
    STM.mainProc()

プログラムの流れとしては、まず、コード下方でStateMachineクラスのインスタンスを生成する。 StateMachineのコンストラクタではMachineクラスのインスタンス生成と各遷移の定義を行う。

# StateMachineコンストラクタ
def __init__(self, name):
    # 名前(使わないと思う)
    self.name = name
    # machineのコンストラクト
    self.machine = Machine(model=self, states=self.states, initial='Entrance', auto_transitions=False)
    # 初期処理実行用遷移の追加
    self.machine.add_transition(trigger='StartState', source='Entrance', dest='MoveExt', before= 'InitProc', after= ['enterMoveExt', 'PrintKeyInput'])

    # keyInput辞書の初期化
    for st in self.states:
        self.keyInput[st] = {}
    
    # 各モードでの遷移の追加
    self.setMoveExtTrans()
    self.setLevelingTrans()

setMoveExtTrans関数では遷移の定義と、キー入力に対する応答をまとめるkeyInput辞書の定義を行う。

# エクストルーダ移動モードの遷移の追加
def setMoveExtTrans(self):
    # ステート名の定義
    state = 'MoveExt'

    # 終了処理への遷移 Exit
    self.machine.add_transition(trigger='Exit', source=state,  dest='Exit', before= 'ExitProc')
    self.keyInput[state][27]={'trigger':'Exit', 'Information':'Exit Process'}

    # レベリングモードへの遷移 switchMode
    self.machine.add_transition(trigger='switchMode', source=state,  dest='Leveling', before= 'SwitchToLevel', after= 'PrintKeyInput')
    self.keyInput[state][ord('b')]={'trigger':'switchMode', 'Information':'change to Leveling Mode'}

    # キー入力処理用遷移 MoveProcess
    self.machine.add_transition(trigger='MoveProcess', source=state,  dest=state, before= 'MoveProc')
    self.keyInput[state][ord('a')]={'trigger':'MoveProcess', 'Information':'Move to X <'} # X <
    self.keyInput[state][ord('d')]={'trigger':'MoveProcess', 'Information':'Move to X >'} # X >
    self.keyInput[state][ord('w')]={'trigger':'MoveProcess', 'Information':'Move to Y ^'} # Y ^
    self.keyInput[state][ord('s')]={'trigger':'MoveProcess', 'Information':'Move to Y v'} # Y v
    self.keyInput[state][ord('q')]={'trigger':'MoveProcess', 'Information':'Move to Z ^'} # Z ^
    self.keyInput[state][ord('e')]={'trigger':'MoveProcess', 'Information':'Move to Z v'} # Z v
    self.keyInput[state][ord('h')]={'trigger':'MoveProcess', 'Information':'Move to home position'} # home position

遷移の追加はadd_transition関数で行う。
keyInput辞書は以下のような構造になっている。入力を受けるとキー入力からトリガー名を調べ、trigger関数で遷移を呼び出すことで、キー入力を介して遷移を実行する。 操作説明用の表示文はInformationに記載する。
{
  [ステート名]:{
    [キー番号]:{
      'trigger':[トリガー名],
      'Informaiton':[表示用の説明文]
    },...
  },...
}
基本的に1つのキー入力に対し1つの遷移が対応するので、遷移を1つ定義したときに同時に追加するようにしている。
移動モードは複数キー入力それぞれに遷移を作ると面倒だったので、呼び出される処理関数で改めてキーを判別し移動方向を決定する。
レベリングモードも同様にして遷移を追加する。

遷移を追加し終わったら、StartStateの遷移し、初期処理を実行する。
その後、無限ループ内で、mainProcを実行する。

# メイン処理
def mainProc(self):
    self.keyBuffer = ord(getch())
    #print(key)
    dict=self.keyInput[self.state].get(self.keyBuffer)
    #print(trig)
    if dict != None:
        #print(dict['trigger'])
        self.trigger(dict['trigger'])
    else:
        print('key input '+chr(self.keyBuffer)+' is invalid')

mainProcではgetchでキー入力を取得し、keyInputから入力に対応するトリガーで遷移する。

まとめ

上のコードで期待通りのプログラムを実装できた。 transitionを使うことで状態遷移が簡単に記述できた。 add_transitionとkeyInputを追加することで、新しい機能を追加できるので、拡張しやすいと思う。 贅沢をいうと、状態ごとにクラスを定義して、そのメソッド関数を呼び出すようにしたかったが、少し面倒だったのであきらめた。