nyabot’s diary

電気猫の夢を見るお話

8速歩行ロボットの歩行プログラム

WebIOPiで動作させるpythonスクリプトについてメモ。

  • 使用サーボはSG90
  • 使用サーボドライバはPCA9685
  • 足は8本、各足2自由度(水平)

基本的なコード、特にPCA9685のセットとWebIOPiとの連携に関しては、以下の書籍の6足歩行ロボットを作る項のコードを参考にさせていただきました。
連携に関する設定等から初心者向けに詳しく書かれているので、毎度のことながら有難み極まる。

自分はプログラムには不慣れなため、感覚的にわかりやすいよう各足ごとに動作させるようにしました。
また、パルスではなく±90で動作角度を指定できるように関数を追加。
少し記述が冗長になってしまうけれど、後から調整もしやすいのでいいかな。

出来上がったコードは以下のような感じ。
これをWebIOPiが起動したときに動作するスクリプトとして設定する。
あとで自分が見返す時のためにコメントましましで。

import webiopi
from time import sleep
import smbus
import math
import threading

def resetPCA9685():
    bus.write_byte_data(address_pca9685, 0x00, 0x00)

# setPCA9685Freq、setPCA9685Duty関数は書籍(別記)より
def setPCA9685Freq(freq):
    ・・・

def setPCA9685Duty(channel, on, off):
    ・・・

# 角度指定でサーボを動かす関数
def setPos(channel, zeroOffset, pos):
    #角度をパルスに変換
    #+90〜-90度がパルス143〜410に相当するようなので、1度あたりのパルスは(410-143)/180
    #角度指定の±90に90を足して0〜180に直し、各サーボごとの補正値zeroOffsetを加えて、最終的な角度に相当するパルスを算出する
    pulse = int(round((410-143)/180*(pos+90+zeroOffset)))+143
    setPCA9685Duty(channel, 0, pulse)

# 動作させる足と動かし方を指定するための関数
def setLegServo(leg, order):
    if leg['Side'] == 'right':
        if order == 'f':
            setPos(leg['Channel_h'], leg['ZeroOffset_h'], -pos_f)
        elif order == 'b':
            setPos(leg['Channel_h'], leg['ZeroOffset_h'], pos_b)
        elif order == 'n':
            setPos(leg['Channel_h'], leg['ZeroOffset_h'], pos_n)
        elif order == 'u':
            setPos(leg['Channel_v'], leg['ZeroOffset_v'], -pos_u)
        elif order == 'd':
            setPos(leg['Channel_v'], leg['ZeroOffset_v'], pos_d)
    elif leg['Side'] == 'left':
        if order == 'f':
            setPos(leg['Channel_h'], leg['ZeroOffset_h'], pos_f)
        elif order == 'b':
            setPos(leg['Channel_h'], leg['ZeroOffset_h'], -pos_b)
        elif order == 'n':
            setPos(leg['Channel_h'], leg['ZeroOffset_h'], pos_n)
        elif order == 'u':
            setPos(leg['Channel_v'], leg['ZeroOffset_v'], pos_u)
        elif order == 'd':
            setPos(leg['Channel_v'], leg['ZeroOffset_v'], -pos_d)

# 足一本ごとに動作を指定するための関数
# 一本の足につき複数のサーボを動かす場合は、第二引数に配列で渡す。
def setLeg(leg, orderList):
    if type(orderList) is str:
        setLegServo(leg, orderList)
    elif type(orderList) is list:
        for order in orderList:
            setLegServo(leg, order)

# モーション設定
def forwardPhase1(delay):
    setLeg(leg_r1, ['u', 'f'])
    setLeg(leg_r3, ['u', 'f'])
    setLeg(leg_l1, ['u', 'f'])
    setLeg(leg_l3, ['u', 'f'])
    setLeg(leg_r2, ['b'])
    setLeg(leg_r4, ['b'])
    setLeg(leg_l2, ['b'])
    setLeg(leg_l4, ['b'])
    sleep(delay)
    setLeg(leg_r1, ['d'])
    setLeg(leg_r3, ['d'])
    setLeg(leg_l1, ['d'])
    setLeg(leg_l3, ['d'])
    sleep(delay)

def forwardPhase2(delay):
    setLeg(leg_r2, ['u', 'f'])
    setLeg(leg_r4, ['u', 'f'])
    setLeg(leg_l2, ['u', 'f'])
    setLeg(leg_l4, ['u', 'f'])
    setLeg(leg_r1, ['b'])
    setLeg(leg_r3, ['b'])
    setLeg(leg_l1, ['b'])
    setLeg(leg_l3, ['b'])
    sleep(delay)
    setLeg(leg_r2, ['d'])
    setLeg(leg_r4, ['d'])
    setLeg(leg_l2, ['d'])
    setLeg(leg_l4, ['d'])
    sleep(delay)

# 前進
def forwardMove(delay):
    forwardPhase1(delay)
    forwardPhase2(delay)

def backwardPhase1(delay):
    setLeg(leg_r1, ['u', 'b'])
    setLeg(leg_r3, ['u', 'b'])
    setLeg(leg_l1, ['u', 'b'])
    setLeg(leg_l3, ['u', 'b'])
    setLeg(leg_r2, ['f'])
    setLeg(leg_r4, ['f'])
    setLeg(leg_l2, ['f'])
    setLeg(leg_l4, ['f'])
    sleep(delay)
    setLeg(leg_r1, ['d'])
    setLeg(leg_r3, ['d'])
    setLeg(leg_l1, ['d'])
    setLeg(leg_l3, ['d'])
    sleep(delay)

def backwardPhase2(delay):
    setLeg(leg_r2, ['u', 'b'])
    setLeg(leg_r4, ['u', 'b'])
    setLeg(leg_l2, ['u', 'b'])
    setLeg(leg_l4, ['u', 'b'])
    setLeg(leg_r1, ['f'])
    setLeg(leg_r3, ['f'])
    setLeg(leg_l1, ['f'])
    setLeg(leg_l3, ['f'])
    sleep(delay)
    setLeg(leg_r2, ['d'])
    setLeg(leg_r4, ['d'])
    setLeg(leg_l2, ['d'])
    setLeg(leg_l4, ['d'])
    sleep(delay)

# 後退
def backwardMove(delay):
    backwardPhase1(delay)
    backwardPhase2(delay)

def rotateRightPhase1(delay):
    setLeg(leg_r1, ['u', 'b'])
    setLeg(leg_r3, ['u', 'b'])
    setLeg(leg_l1, ['u', 'f'])
    setLeg(leg_l3, ['u', 'f'])
    setLeg(leg_r2, ['f'])
    setLeg(leg_r4, ['f'])
    setLeg(leg_l2, ['b'])
    setLeg(leg_l4, ['b'])
    sleep(delay)
    setLeg(leg_r1, ['d'])
    setLeg(leg_r3, ['d'])
    setLeg(leg_l1, ['d'])
    setLeg(leg_l3, ['d'])
    sleep(delay)

def rotateRightPhase2(delay):
    setLeg(leg_r2, ['u', 'b'])
    setLeg(leg_r4, ['u', 'b'])
    setLeg(leg_l2, ['u', 'f'])
    setLeg(leg_l4, ['u', 'f'])
    setLeg(leg_r1, ['f'])
    setLeg(leg_r3, ['f'])
    setLeg(leg_l1, ['b'])
    setLeg(leg_l3, ['b'])
    sleep(delay)
    setLeg(leg_r2, ['d'])
    setLeg(leg_r4, ['d'])
    setLeg(leg_l2, ['d'])
    setLeg(leg_l4, ['d'])
    sleep(delay)

# 右旋回
def rotateRightMove(delay):
    rotateRightPhase1(delay)
    rotateRightPhase2(delay)

def rotateLeftPhase1(delay):
    setLeg(leg_r1, ['u', 'f'])
    setLeg(leg_r3, ['u', 'f'])
    setLeg(leg_l1, ['u', 'b'])
    setLeg(leg_l3, ['u', 'b'])
    setLeg(leg_r2, ['b'])
    setLeg(leg_r4, ['b'])
    setLeg(leg_l2, ['f'])
    setLeg(leg_l4, ['f'])
    sleep(delay)
    setLeg(leg_r1, ['d'])
    setLeg(leg_r3, ['d'])
    setLeg(leg_l1, ['d'])
    setLeg(leg_l3, ['d'])
    sleep(delay)

def rotateLeftPhase2(delay):
    setLeg(leg_r2, ['u', 'f'])
    setLeg(leg_r4, ['u', 'f'])
    setLeg(leg_l2, ['u', 'b'])
    setLeg(leg_l4, ['u', 'b'])
    setLeg(leg_r1, ['b'])
    setLeg(leg_r3, ['b'])
    setLeg(leg_l1, ['f'])
    setLeg(leg_l3, ['f'])
    sleep(delay)
    setLeg(leg_r2, ['d'])
    setLeg(leg_r4, ['d'])
    setLeg(leg_l2, ['d'])
    setLeg(leg_l4, ['d'])
    sleep(delay)

# 左旋回
def rotateLeftMove(delay):
    rotateLeftPhase1(delay)
    rotateLeftPhase2(delay)

# 停止
def stopMove(delay):
    setLeg(leg_r1, ['n', 'd'])
    setLeg(leg_r2, ['n', 'd'])
    setLeg(leg_r3, ['n', 'd'])
    setLeg(leg_r4, ['n', 'd'])
    setLeg(leg_l1, ['n', 'd'])
    setLeg(leg_l2, ['n', 'd'])
    setLeg(leg_l3, ['n', 'd'])
    setLeg(leg_l4, ['n', 'd'])
    sleep(delay)

def processCommands():
    global status

    while True:
        if status == 'i':
            sleep(delay)
        elif status == 'f':
            forwardMove(delay)
        elif status == 'b':
            backwardMove(delay)
        elif status == 'r':
            rotateRightMove(delay)
        elif status == 'l':
            rotateLeftMove(delay)
        elif status == 's':
            stopMove(delay)
            status = 'i'
        elif status == 'k':
            break

# 各足に対応する変数に、向き、サーボのチャンネル、角度補正値を連想配列で格納
# サーボは取り付けるときに0度位置に合わせるが、少しずれるのでここで補正する
# 水平方向に動かすサーボはCannel_h, ZeroIffset_h, 垂直方向に動かすサーボはChannel_v, ZeroOffset_vをそれぞれキーにする
leg_r1 = {'Side' : 'right', 'Channel_h' : 0, 'ZeroOffset_h' : -5, 'Channel_v' : 1, 'ZeroOffset_v' : 5}
leg_r2 = {'Side' : 'right', 'Channel_h' : 2, 'ZeroOffset_h' : 10, 'Channel_v' : 3, 'ZeroOffset_v' : -5}
leg_r3 = {'Side' : 'right', 'Channel_h' : 4, 'ZeroOffset_h' : 0, 'Channel_v' :5 , 'ZeroOffset_v' :0 }
leg_r4 = {'Side' : 'right', 'Channel_h' : 6, 'ZeroOffset_h' :0 , 'Channel_v' :7 , 'ZeroOffset_v' :-5 }
leg_l1 = {'Side' : 'left', 'Channel_h' : 8, 'ZeroOffset_h' :0, 'Channel_v' :9 , 'ZeroOffset_v' :-10 }
leg_l2 = {'Side' : 'left', 'Channel_h' : 10, 'ZeroOffset_h' : -5, 'Channel_v' :11 , 'ZeroOffset_v' :-5 }
leg_l3 = {'Side' : 'left', 'Channel_h' : 12, 'ZeroOffset_h' : -5, 'Channel_v' :13 , 'ZeroOffset_v' :5 }
leg_l4 = {'Side' : 'left', 'Channel_h' : 14, 'ZeroOffset_h' : 0, 'Channel_v' :15 , 'ZeroOffset_v' :5 }

# 各動作に応じたサーボの動作角度設定
# 水平方向 n: neutral, f: forward, b: backward
# 垂直方向 u: up, d: down
pos_n = 0
pos_f = 18
pos_b = 18
pos_u = 30
pos_d = 0

delay = 0.15

# i:idle, f: forward, b: backward, r: right, l: left, s: stop, k: kill
status = 'i'

bus = smbus.SMBus(1)
address_pca9685 = 0x40

resetPCA9685()
setPCA9685Freq(50)

stopMove(delay)

t = threading.Thread(target=processCommands)
t.start()

# デバッグ出力を有効に
webiopi.setDebug()

# WebIOPiの起動時に呼ばれる関数
def setup():
    webiopi.debug("Script with macros - Setup")

# WebIOPiにより繰り返される関数
def loop():
    webiopi.sleep(5)

# WebIOPi終了時に呼ばれる関数
def destroy():
    webiopi.debug("Script with macros - Destroy")
    global status
    status = 'k'
    t.join()

@webiopi.macro
def setLegsAction(action, commandID):
    global status
    status = action

足を運ぶ順番や角度の指定を少し変えると、また違った動きになって楽しい。 単なる前進でも足運びの方法が多数あるのも、多足ロボットならではの面白さかも。

なお、他の一般的なサーボは動作角度が+-逆らしい。Mk-Ⅱを作るときは気をつける。

つづく。