ブログ トピックス

ニュース

    USB2DXIF入荷のお知らせ (2024/05/02)

    大変お待たせいたしました。
    PCとDynamixelシリーズ間を仲介するI/F「BTE101 USB2DXIF」が入荷いたしましたので、販売を再開いたしました。


    管理部

    年末年始休業のお知らせ (2023/12/18)

    2023年12月28日(木)~2024年1月4日(木)の間は、年末年始休業とさせていただきます。
    • 電話・FAXによるお問い合わせはお休みいたします。
    • 電子メールによるお問い合わせ及び、オンラインショップからのご注文は、2023年1月5日以降から順次対応いたします。
    • Dynamixelシリーズについては12月22日までのオーダ分を今月の最終注文といたします。
    何卒ご了承くださいますようお願い申し上げます。

    年末年始休業のお知らせ (2022/12/15)

    2022年12月28日(水)~2023年1月4日(水)の間は、年末年始休業とさせていただきます。
    • 電話・FAXによるお問い合わせはお休みいたします。
    • 電子メールによるお問い合わせ及び、オンラインショップからのご注文は、2023年1月5日以降から順次対応いたします。
    何卒ご了承くださいますようお願い申し上げます。

ニュースのトップへ

ブログ

    DXL Yシリーズ (2024/06/17)

    リリースされたのに詳細の案内を行っていなかったDXL Yシリーズ
    少々高価なので手が出にくいが、210W/230WのBLDC・高分解能なエンコーダ・絶対位置のバッテリバックアップ、といったPシリーズを置き換えるには十分すぎる性能を持っているので、これらも後ほどRasberry Pi Zero 2 Wを使って動かしてみようと思う。



    技術

    PMXネタつながりで (2024/04/25)

    事のついでで恐縮だが、キャリアボードの評価をお願いしている方からKO B3Mがマトモに動かないという指摘があったので、そのついでに作っておいた。10年前にこねくり回すのを諦めてから重い腰を上げて弄ったが、結局のところB3Mの癖は拭い切れないまま。

    こちらも事のついでだが、少し毛色の違うサーボを引っ張り出して触っておいたレポートを。

    外観が何かに酷似しているのは気のせいとして、サーボモータとして最低限の制御を行うついでに、テキストベースのプロトコルを被せているところが毛色の違いかと。ようするに俺サーボ。
    適当なシリアルターミナル上でポチポチキーボードを打てばとりあえず動くが、プログラムからテキストで通信させるとなると却って面倒。こちらも例に漏れずPythonで記述するためのクラスが用意されているが、シリアル通信部分を隠蔽するだけで実質何もしておらず、結局正規表現任せ。

    これを2個使い相互リンクをやらせてみた(単に互いの位置偏差を電流指令しているだけだが)。実機を持つ方はたかが知れているのでコードを晒すまでもないが、2軸への電流制御モード設定・ゲイン設定・電流指令・現在値取得が1パケットで済み、要点のみであれば実質数行で済む(PeriodicTimerは周期タイマクラス)。
    #!/usr/bin/python3
    #
    # Link two servos with virtual wires

    import os, platform, psutil, time, kbhit, serial, numpy as np
    from PeriodicTimer import PeriodicTimer
    from jp200class import packet

    intervalms = 20

    try:
    pr = psutil.Process(os.getpid())
    if platform.system() == 'Windows':
    pr.nice(psutil.HIGH_PRIORITY_CLASS)
    else:
    pr.nice(-10)
    except:
    print('info:run with sudo')

    # present value
    present_dt = [('ang',float),('cur',float),('velo',float)]
    present = np.empty(2, dtype = present_dt)
    # calculated current command value
    tcur = [0.0] * 2

    #--------------------------------
    # Periodically executed functioin
    #--------------------------------
    def cycfunc(*p):
    sv = p[0]
    acks = []
    # deviation from each other's position
    tcur = (
    np.clip((present[1]['ang'] - present[0]['ang']) * 0.15, -1000.0, 1000.0),
    np.clip((present[0]['ang'] - present[1]['ang']) * 0.15, -1000.0, 1000.0)
    )
    # send current command and current position acquisition to JP200
    sv.request(
    '#0EX=4SG2=9000;5500;10TC=%dTP=1000CACVCC'
    '#1EX=4SG2=9000;5500;10TC=%dTP=1000CACVCC'
    %(tcur[0], tcur[1]), acks
    )
    # analyze response
    if acks:
    for ack in acks:
    m = sv.p2.findall(ack)
    id = -1
    for n in m:
    try:
    if n[0] == '#' and int(n[1]) >= 0: id = int(n[1])
    elif n[0] == 'CA' and id >= 0: present[id]['ang'] = float(n[1])
    elif n[0] == 'CC' and id >= 0: present[id]['cur'] = float(n[1])
    elif n[0] == 'CV' and id >= 0: present[id]['velo'] = float(n[1])
    except:
    pass
    if id >= 0: print('[%d] %4d %8d %5d %5d '%(id, tcur[id], present[id]['ang'], present[id]['velo'], present[id]['cur']), end = '')
    print(end = '\r')

    # instantiate kbhit
    kb = kbhit.KBHit()
    # open UART
    ser = serial.Serial('/dev/ttyAMA0', 2000000)
    print(ser.name)
    # instantiate JP200
    sv = packet(ser, 0.02)
    # instantiate periodic timer & start
    tm = PeriodicTimer(intervalms / 1000, cycfunc, [sv])

    acks = []
    k = ''
    while k != 'q':
    # waiting for key input
    try:
    k = kb.getch()
    except UnicodeDecodeError:
    pass
    print(k, end = '\n', flush = True)

    # move to origin position
    if k == 'h':
    tm.stop()
    sv.request('#0EX=1TA=18000TP=1000#1EX=1TA=18000TP=1000', acks)
    # stop timer
    elif k == 's':
    tm.stop()
    tm.start()
    # start timer
    elif k == 'e':
    tm.stop()
    sv.request('#0EX=0#1EX=0', acks)
    elif k == 'c':
    tm.stop()
    sv.request('#0EX=8TP=1000#1EX=8TP=1000', acks)
    time.sleep(5)
    sv.request('#0EX=8TP=-1000#1EX=8TP=-1000', acks)
    time.sleep(5)
    sv.request('#0EX=0#1EX=0', acks)
    else:
    print('XXXXXXXXX')
    tm.stop()

    sv.request('#0EX=0#1EX=0', acks, True)
    ser.close()
    kb.set_normal_term()
    del sv, ser, kb

    os._exit(0)
    今回はRZ/V2LのDRP-AIを評価したベンチで使ったサーボをZero 2 Wでも試してみたに過ぎないが、こんな具合に使える(長いだけで大して面白くないので再生するまでもなく)。

    事のついでと書いている大本の理由は、ここ最近XL330/XC330が軒並み通信できないベンチに遭遇して痛い目にあっており、動かすチャンスがあるときは一通り試しておかないと気が気でないものだから(XC330/XL330はボーレートの許容誤差が狭い)。

    技術

    KONDO PMX試食3 (2024/04/18)

    前回に引き続き小出し第3弾。

    その前に、今までPMXのボーレートを3Mbpsとしていたが、バックグラウンドで動かしているアプリケーションによって受信データの欠損やズレが散見されていた。そのために態々リトライする関数を用意した訳だが。
    原因を追求すべく色々試した結果、Zero 2の都合で時折受信データを取りこぼしていると考えられ、最終的にはボーレートにも依存するところまで判明した。OSが過負荷状態であっても、今のところ1.5Mbps以下であれば受信データを取りこぼさないようだ。

    それでは本題に。
    前回の漫画にどうしてXC330-T181-Tがつながっていたかの答えでもあるが、常々やってみたかったのが、異なる異メーカのサーボを同時に使うという少々危険なネタ。
    これをやってみようと思ったのは、DXLに用意されているV1/V2の通信プロトコルを混在させている実例があったのと、PMXの通信プロトコルがB3MやKRSシリーズのプロトコルに比べ格段に堅牢になっていたからでもある。また単一メーカに依存して無い物ねだりする事を無くす事も狙い。
    ちなみにB3MやKRSでは深く考えるまでもなく絶対的に無理、というか危険極まりないのでやってはならない。またDXLもV1とV2を混在させるとV1側の挙動がおかしくなるのは既知なので、実質的に無理。プロトコルの混在を実現するには、各サーボが自身には無関係なパケットを即時破棄する事と、自身に関係のあるパケットを取りこぼさないというそこそこ厳しい条件が求められる。それらは当然こちらでどうにかできる話ではないので、完全に自己責任のネタである。

    では徐にやってみる。
    PMXはID=0でボーレートを1Mbps、DXLはID=1でボーレートを1Mbpsとした。そもそもプロトコルが異なるのでお互いにIDが同じであっても構わないだろう。ライブラリは前回同様にpyPMX.pypyDXL.pyを使用。以下は1つのコードになっているか、PMXとDXLを各々個別のスレッドで処理させ、互いの存在は全く意識していない。オープンしたポートのインスタンスとLockを両スレッド共通に使用している。いずれも正弦波を元に位置決め制御を行いつつ、現在位置を取得してモニタ用の変数に書き込んでいる。ループ中には適当な時間のsleepを入れておいた方が良いだろう。
    #!/usr/bin/python3
    from pyPMX import *
    from pyDXL import *
    import math, kbhit, time, serial, threading

    # Communication task with PMX
    def func1(arg):
    id = 0
    # Class instantiation
    pmx = PMXProtocol(arg[0], lock = arg[1])
    # Setting initial conditions
    pmx.MemREAD(id, 400, 6) # Clear error flag
    pmx.MotorWRITE(id, 2, ()) # Torque free
    pmx.MemWRITE8(id, 501, (1, 0b11111)) # Control mode (All)
    pmx.MotorWRITE(id, 1, ()) # Torque enable
    while arg[2]:
    ang = int(math.sin(time.time()) * 9000.0)
    r = pmx.MotorWRITE(id, 0, (ang,)) # Angle command
    if r != None:
    arg[3] = r[1][0]
    time.sleep(0.001)
    pmx.MotorWRITE(id, 2, ()) # Torque free
    del pmx

    # Communication task with DXL
    def func2(arg):
    id = 1
    cnt = 0
    led = 0
    # Class instantiation
    dx2 = DXLProtocolV2(arg[0], lock = arg[1])
    # Setting initial conditions
    dx2.Write8(id, 64, 1) # Torque enable
    while arg[2]:
    ang = int(math.sin(time.time()) * 1024.0) + 2047
    dx2.Write32(id, 116, ang) # Angle command
    r = dx2.Read32(id, 132, signed = True) # Read feedback values
    if r != None:
    arg[4] = r
    cnt += 1
    if cnt % 10 == 0:
    led ^= 1
    dx2.Write8(id, 65, led) # Update LED status
    time.sleep(0.001)
    dx2.Write8(id, 65, 0) # LED off
    dx2.Write8(id, 64, 0) # Torque free
    del dx2

    kb = kbhit.KBHit()
    # Open serial port
    com = serial.Serial('/dev/ttyAMA0', 1000000, timeout=0.005)
    lock = threading.Lock()
    act = True
    v1 = 0
    v2 = 0
    SharedVar = [com, lock, act, v1, v2]

    # Start thread
    th1 = threading.Thread(target = func1, args = [SharedVar])
    th2 = threading.Thread(target = func2, args = [SharedVar])
    th1.start()
    th2.start()

    # Loop until keyboard input
    while True:
    if kb.kbhit():
    break
    print(f' pmx:{SharedVar[3]:6d}, dxl:{SharedVar[4]:6d}', end = '\r')
    time.sleep(0.05)

    # End
    SharedVar[2] = False
    th1.join()
    th2.join()

    del kb, lock, com, th1, th2
    print()
    結果、いずれのサーボもスムーズに動くし、更に長時間運転してみても各関数はエラーを吐いていない。
    色々虐め始めるとボロも出て来るだろうが、サーボそのものが不可解な挙動をしない限り、もしかしたら新たな使い道が見い出せるかも知れない。

    キャリアボードに装備した機能の検証を含んだ操作している様子を動画にしておいた。

    以上で久々に新鮮な気持ちで遊興したレポートを終える。

    技術

    KONDO PMX試食2 (2024/04/16)

    前回に引き続きPMX。

    各装置の接続は最終的にこのような構成に。
    キャリアボードに供給する電源が内蔵のスイッチ回路を介してEHコネクタからサーボへ供給される。
    Raspberry Pi Zero 2 Wの操作はモニタとキーボードを直接接続して行うも良し、USB経由のRNDISでリモートログインしても良し、WiFi経由でリモートログインしても良し。
    これでハードウェアの準備は完了。

    ではようやくソフトウェアに。
    Raspberry Pi Zero 2 WをホストとしたのでLinuxが前提、Zeroは極めてメモリが少ないのでCLIを基本とした。今回はモータが動きさえすれば良いので、プログラムのデバッグはキャラクタベースで十分と判断。
    キャリアボードはRaspberry Piの機能を利用するので、本家のドキュメントDXHATの使用方法を元に設定。念のためcmdline.txtからconsoleの記述が消えているのを確認しておこう。
    キャリアボードのRS-485 I/FはZeroの「/dev/ttyAMA0」に割り当てられる。
    この辺は先人の知恵を借りれば良し。

    次にPythonによるPMXとの通信だが、既にDXL用Pythonライブラリが公開されているのでその構成を真似ね、PMXのプロトコルで規定されているコマンドに応じた関数を用意し、こちらでpyPMX.pyとして公開。あると便利な検索・ID変更・ボーレート変更の各スクリプトも事のついでに作成。これにてオンラインマニュアル片手にポチポチと弄る準備が完了。
    一応ライブラリの末尾にテストコードを仕込んでおいたが、PMXに位置決め制御を行わせるだけであれば、以下のように動作モードを設定しTorqueONした後にMotorWRITEで角度を指令するだけ。
    #!/usr/bin/python3
    import time
    from pyPMX import PMXProtocol

    ID = 0
    BAUD = 3000000
    # Create pmx instance
    pmx = PMXProtocol('/dev/ttyAMA0', BAUD)

    pmx.MemREAD(ID, 400, 6) # Error clear
    pmx.MotorWRITE(ID, pmx.MOTW_OPT_FREE, ()) # Torque off
    pmx.MemWRITE8(ID, 501, 1) # bit 0:pos,1:speed,2:cur,3:torq,4:pwm,5:time
    pmx.MotorWRITE(ID, pmx.MOTW_OPT_TORQUEON, ()) # Torque on

    for ang in (0, 4500, 9000, 4500, 0, -4500, -9000, -4500, 0):
    pmx.MotorWRITE(ID, pmx.MOTW_OPT_NONE, (ang,)) # Command angle
    time.sleep(0.5)
    pmx.MotorWRITE(ID, pmx.MOTW_OPT_FREE, ()) # Torque off
    実はこの程度の処理にもかかわらず、想定した動作を行ってくれない事が散見された。各関数の成否を確認しないまま次の処理に遷移させているため、初期設定の所で何かしら失敗したまま角度の指令がなされている時に動かなかった様だ。
    それがキャリアボードの問題なのか通信方法の問題なのかの切り分けはしていないが、関数の返り値を確認してリトライする等の処理を加えれば、とりあえずお茶は濁せそうだ。

    それらを意識した上で複数IDを一括で処置する関数を作成、少しだけコンソール上での操作をしやすくするためにkbhitを導入、最終的に2軸を対象にしたプログラムを作ってみた。
    #!/usr/bin/python3

    _retrynum = 3
    # mode set & torque enable
    def _setmode(pmx, id, m):
    for i in range(_retrynum):
    if pmx.MemWRITE8(id, 500, pmx.MOTW_OPT_FREE):
    if m != 0:
    # 1:pos,2:speed,4:cur,8:torq,16:pwm,32:time
    if pmx.MemWRITE8(id, 501, m):
    if pmx.MemWRITE8(id, 502, 0b11111):
    if pmx.MemWRITE8(id, 503, 5):
    if pmx.MemWRITE8(id, 500, pmx.MOTW_OPT_TORQUEON):
    return True
    else:
    return True
    return False

    def SetMode(pmx, ids, m):
    if isinstance(ids, list) or isinstance(ids, tuple):
    r = 0
    for i, id in enumerate(ids):
    if isinstance(m, list) or isinstance(m, tuple):
    r += 1 if _setmode(pmx, id, m[i]) else 0
    else:
    r += 1 if _setmode(pmx, id, m) else 0
    return True if r == len(ids) else False
    else:
    return _setmode(pmx, ids, m)

    # set angle
    def _setangle(pmx, id, ang, t):
    for i in range(_retrynum):
    r = pmx.MotorWRITE(id, pmx.MOTW_OPT_NONE, (int(ang * 100), t))
    if r != None:
    if len(r) > 0:
    return r[1]
    return ()

    def SetAngle(pmx, ids, angs, t):
    if isinstance(ids, list) or isinstance(ids, tuple):
    r = ()
    for i, id in enumerate(ids):
    if isinstance(angs, list) or isinstance(angs, tuple):
    r += _setangle(pmx, id, angs[i], t),
    else:
    r += _setangle(pmx, id, angs, t),
    return r
    else:
    return _setangle(pmx, ids, angs, t)

    # set velocity
    def _setvelocity(pmx, id, velo):
    for i in range(_retrynum):
    r = pmx.MotorWRITE(id, pmx.MOTW_OPT_NONE, (int(velo),))
    if r != None:
    if len(r) > 0:
    return r[1]
    return ()

    def SetVelocity(pmx, ids, velos):
    if isinstance(ids, list) or isinstance(ids, tuple):
    r = ()
    for i, id in enumerate(ids):
    if isinstance(velos, list) or isinstance(velos, tuple):
    r += _setvelocity(pmx, id, velos[i]),
    else:
    r += _setvelocity(pmx, id, velos),
    return r
    else:
    return _setvelo(pmx, ids, velos)

    def _getpresent(pmx, id):
    for i in range(_retrynum):
    r = pmx.MemREAD16(id, 300, 5, signed = True)
    if r != None:
    return r
    return ()

    def GetPresent(pmx, ids):
    if isinstance(ids, list) or isinstance(ids, tuple):
    r = ()
    for i, id in enumerate(ids):
    r += _getpresent(pmx, id),
    return r
    else:
    return _getpresent(pmx, ids)

    if __name__ == "__main__":
    import os, sys, time, kbhit
    from pyPMX import PMXProtocol

    kb = kbhit.KBHit()
    pmx = PMXProtocol('/dev/ttyAMA0', 3000000, 0.005)

    ID1 = 0
    ID2 = 1

    # Position Control
    print('Position Control')
    print(f'{ID1}:angle velo cur torque pwm {ID2}:angle velo cur torque pwm')
    if SetMode(pmx, (ID1, ID2), 1|32):
    while not kb.kbhit():
    t = 1000
    for ang in (0, 45, 90, 45, 0, -45, -90, -45):
    if kb.kbhit(): break
    SetAngle(pmx, (ID1, ID2), (ang, ang * 2), t)
    s = time.time() + t / 1000.0
    while s > time.time():
    if kb.kbhit(): break
    r = GetPresent(pmx, (ID1, ID2))
    for r in r:
    print(('{:6d} '*len(r)).format(*r), end='')
    print(end='\r')
    SetMode(pmx, (ID1, ID2), 0)
    kb.getch()

    # Velocity Control
    print('\nVelocity Control')
    print(f'{ID1}:angle velo cur torque pwm {ID2}:angle velo cur torque pwm')
    if SetMode(pmx, (ID1, ID2), 2):
    while not kb.kbhit():
    t = 3000
    for velo in (0, 500, 1000, 1500, 1000, 500, 0, -500, -1000, -1500, -1000, -500):
    if kb.kbhit(): break
    SetVelocity(pmx, (ID1, ID2), (velo*1, velo*1.5))
    s = time.time() + t / 1000.0
    while s > time.time():
    if kb.kbhit(): break
    r = GetPresent(pmx, (ID1, ID2))
    for r in r:
    print(('{:6d} '*len(r)).format(*r), end='')
    print(end='\r')
    SetMode(pmx, (ID1, ID2), 0)

    del kb, pmx
    適当な指令値を元に位置決め制御と速度制御を行い、コンソール上で何か入力すると処理を推移する。制御中は読み出した現在値をバラバラとコンソールに吐き出し続ける。
    紹介したコードはいらぬものも含んでいるので少々長くなっているが、SetModeを行って成功したらSetAngleやSetVelocityを行っているだけの事である。

    Zeroを一通り操作している様子をラズパイカメラで自撮りしておいた。

    ちなみに先の関数エラーはRS-485トランシーバの電気的な問題だったようで、真面目にターミネータの他にプルアップとプルダウン抵抗を装備して事なきを得た。

    技術

ブログのトップへ