マウスでロボットアームを制御してみる【Python】
投稿日:2020年10月14日 更新日:

Raspberry piを使ってサーボモータを動かしてみよう、そしてロボットアームを制御してみようと思いまして、作製した手順をまとめてみようと思います。ロボットアームはマウスで動くようにしてみました。
構成
材料
- Raspberry Pi 3 Model B
- ロボットアーム 6軸
- DC電源 5V

今回使用したロボットアームは、MG995サーボ6個で構成されたものになります。Amazonから買いました(写真ではシルバーですが実際はブラックでした)。サーボモータ6つ付いて7500円ほどなので、お買い得です。サーボMG995は駆動電圧4.8~6.6Vなので、DC電源は1.5V電池4本でも一応動きますが、結構電力を使うようなのでコンセントから給電できるDC電源を用意すると安心です。私はタブレットに付属していた5V3A出力のDCアダプターを使用しています。
ソフトウェア
- OS: Raspberry Pi OS (旧 Raspbian)
- Python: version 3.7
- ライブラリ- pigpio (サーボ制御のためのGPIO出力制御用)
- WebIOPi(ブラウザからPythonの実行用)
 
pigpioとWebIOPiのインストール方法は以下の通りです。サーボモータへ入力するパルス信号の制御(PWM)用のモジュールは、wiringpiというのもありますが、pigpioの方が精度が高いようなのでpigpioを使っています。
全体像
マウスでサーボモータを動かす流れは、
- ブラウザでjavascriptを使ってマウス操作の情報を取得
- WebIOPiを使って、1で取得した値をPythonプログラムに渡す
- Pythonからpigpioを使ってラズパイのGPIOからパルス信号出力
- パルス信号を受け取ったサーボモータが動く
数値入力によるサーボモータの制御
pigpioのインストール
aptコマンドでpigpioをインストールできます。Raspberry Piの端末から以下を実行します。
sudo apt install pigpio
これでpigpioを使ったGPIO制御が可能になります。今回はPythonを使ってこのpigpioを使用します。
配線
Raspberry PiのGPIO、サーボモータ、DC電源の接続は下図の通りです。サーボモータには以下の3本の線を接続します。
- 茶色:GND
- 赤色:4.8 ~ 6.6V
- 橙色:Raspberry PiのGPIO

Pythonからpigpioを使ったサーボモータ動作
pigpioをPythonで動かし、Raspberry PiのGPIOからパルス信号を出してみます。下記のようなPythonコードでファイルを作成します。ここでは例としてservo.pyというファイル名とします。
import time
import pigpio
pi = pigpio.pi()
try:
    while True:
        val = input('Enter number (0-180): ') # サーボモータの角度を入力
        val = int(val)
        val = int(1900/180*val) + 500   # 500 - 2400
        print(val)
        pi.set_servo_pulsewidth(21, val)  # gpio21からのパルス信号を設定
        time.sleep(0.25)
except KeyboardInterrupt: # Ctrl + c で停止
    pi.set_servo_pulsewidth(21, 0)
    pi.stop()
    print("stop") 
作成したPythonのプログラムをRaspberry Piの端末で実行します。
$ python3 servo.py
するとキーボード入力待ち状態になりますので、目的のサーボモータの角度(0~180度の間)を入力すると、サーボモータが動きます。 たとえば45度にしたいときは入力待ち状態で、”45″ + Enterキー と入力します。
マウス操作によるサーボモータの制御
数値入力によってサーボ角度の制御ができたので、次にマウスカーソルの座標からサーボモータを動かしたいと思います。マウス操作を取り扱うにはGUI(グラフィカル・ユーザー・インターフェイス)を用意しなければいけません。今回は手っ取り早くブラウザを使ってみます。ブラウザからマウスカーソルの座標を取得し、Pythonプログラムに受け渡してサーボモータを動かすという流れになります。配線は「数値入力によるサーボモータの制御」の時と同じで問題ありません。
WebIOPiのインストール
ブラウザからRaspberry Pi内にアクセスするのに、WebIOPiというのを使います。WebIOPiをインストールするには端末で以下を入力します。
$ cd ~
$ wget https://sourceforge.net/projects/webiopi/files/WebIOPi-0.7.1.tar.gz
$ tar xvzf WebIOPi-0.7.1.tar.gz
$ cd WebIOPi-0.7.1
ここで初期のRaspberry Pi向けであるWebIOPiをRaspbery Pi 3でも使用できるようにパッチを適用します。
$ wget https://raw.githubusercontent.com/doublebind/raspi/master/webiopi-pi2bplus.patch
$ patch -p1 -i webiopi-pi2bplus.patch
$ sudo ./setup.sh
「Do you want to access WebIOPi over Internet ? [y/n]」と聞かれたら「n」と入力すればよいです(「y」を選ぶと、外部ネットワークからRaspberry PiにアクセスできるWeavedのIoT Kitというものがインストールされるようです)。
続いてWebIOPiをサービスとしてsystemctlで操作できるようにします。
$ cd /etc/systemd/system/
$ sudo wget https://raw.githubusercontent.com/doublebind/raspi/master/webiopi.service
WebIOPiサービスの開始、自動起動ONにするには
$sudo systemctl start webiopi
$sudo systemctl enable webiopi
WebIOPiの設定
WebIOPiの設定ファイルの中身を書き加えます。
$ sudo vi /etc/webiopi/config
#でコメントアウトしてある部分を参考に、doc-rootとmyscriptのパスを指定します。doc-rootはブラウザ(html)ファイルを置く場所、myscriptはブラウザから実行するPythonプログラムを指定します。ここではユーザー名がpiで、ホームディレクトリの”webiopi”というディレクトリをプログラムの置く場所とした例を示しています。
・
・
[HTTP]
・
・
# Use doc-root to change default HTML and resource files location
#doc-root = /home/pi/webiopi/examples/scripts/macros
doc-root = /home/pi/webiopi/
・
・
・
[SCRIPTS]
・
・
#   each sourcefile may have setup, loop and destroy functions and macros
#myscript = /home/pi/webiopi/examples/scripts/macros/script.py
myscript = /home/pi/webiopi/pwmservo.py
プログラムの用意
ブラウザで表示させるindex.htmlファイルと、そこから実行させるサーバー上のPythonファイルを準備します。これらはWebIOPyの設定ファイルである/etc/webiopi/config内に記述したものと対応します。上での例にしたがって、index.htmlとPythonプログラムは/home/pi/webiopiディレクトリへ置くことにして、Pythonプログラム名はpwmservo.pyとしています。
<!doctype html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>WebIOPi | Servo controller</title>
        <script type="text/javascript" src="/webiopi.js"></script>
        <script type="text/javascript">
           webiopi()
           var timer;
           
           window.addEventListener("load", function () {
              var cursorX = 0;
              var w = 0;
             // PWM値をpythonプログラムに渡す           
             function main() {
               var pwm1 = Math.round(1900 * cursorX/w) + 500;
               webiopi().callMacro("run_script", [pwm1], callbackGetValue);
             }
             
              // Window上のマウス座標を取得する
              var mouseMove = function (e) {
                 if (e) {
                  cursorX = e.pageX;
                 } else {
                  cursorX = event.pageX;
                 }
              }
             document.addEventListener("mousemove",mouseMove);
             
               
            // Windowサイズを取得する
             w = window.innerWidth;
            
            window.onresize = function (e) {
             w = window.innerWidth;
            }
           
             timer = setInterval(main, 550); // 550msごとにpythonプログラム実行
           })
           
           // ラズパイからの信号を止める
           function stopArm() {
               clearInterval(timer);
               webiopi().callMacro("run_script", [0], callbackGetValue);
           }
           function callbackGetValue(macro, args, data) {
               console.log(macro);
               console.log(args);
               console.log(data);
           }
        </script>
    </head>
    <body>
        <input type="button" value="stop" class="off" onClick="stopArm()">
        <p>再スタート:F5 or Ctrl+r</p> 
    </body>
</html>
import webiopi
import sys
import os
import time
import pigpio
sd = os.path.dirname(os.path.abspath(__file__))
sys.path.append(sd)
PIN1 = 21
SLEEP_TIME = 0.05
pi = pigpio.pi() # Connect to local Pi.
@webiopi.macro
def run_script(pwm1):
     pi.set_servo_pulsewidth(PIN1, pwm1)
     time.sleep(SLEEP_TIME)
大まかな動作の流れ
- 関数mouseMove(index.html):マウスカーソルのx座標を取得
- 変数pwm1(index.html):x座標から決めたPWM値(サーボMG995では500~2400が駆動範囲)
- webiopi().callMacro(…)(index.html):サーバ上のPythonコード中を実行
- pi.set_servo_pulsewidth(PIN1, pwm1)(pwmservo.py):GPIOのPIN1から周波数PWM1のパルス信号を出力
- timer = setInterval(main, 550);(index.html):1 ~ 4を500m秒間隔で繰り返し実行
index.html内に記述してあるjavascriptを使って、マウス座標を取得しPWM値を計算して、GPIOを動作させるPythonプログラムに渡すことで、マウスからサーボモータを操作できるようにしています。javascript上でPythonコードを実行しているのは
webiopi().callMacro(“実行するPythonの関数”, [Pythonの関数に渡す引数], Pythonの関数からのリターン値を渡すjavascript関数);
の部分です
プログラムの実行
WebIOPiの再起動
PythonコードをWebIOPiに反映させるには、webiopiサービスを再起動させる必要があるため端末上で
$sudo systemctl restart webiopi
と入力する必要があります。これはPythonのコードを書き換えるたびに必要です。
ブラウザ上で動作確認
実際に動かすには、Raspberry Pi自身のブラウザで”http://localhost:8000“にアクセスします。同じローカルネットワーク内のPCからも”http://<Raspberry PiのIPアドレス>:8000“で同様にアクセスできます。認証ダイアログが表示されたら
- ユーザ名:webiopi
- パスワード:raspberry
でログインでき、index.htmlにアクセスできます。
アクセスしてプログラムの読み込みが完了すると、サーボモータにPWMパルスを出力しだします。ブラウザ上でのマウスのx座標(横方向)を読み取り、その値に対応してサーボの角度が変化するはずです。
ブラウザ上の”stop”ボタンを押すとGPIOからパルス信号がでなくなり、サーボモータのパワーがオフになります。
マウスによるロボットアームの制御

マウスカーソル移動によって1軸のサーボモータを動かすことができました。次にこれを6軸に拡張して、ロボットアームを動かしてみます。
プログラムの用意
上記の1軸の時に使ったindex.htmlとpwmservo.pyの中身をそれぞれ以下のように書き換えます。
<!doctype html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>WebIOPi | Servo control<</title>
        <script type="text/javascript" src="/webiopi.js"&<</script>
        <script type="text/javascript">
           webiopi()
           var timer;
           
           window.addEventListener("load", function () {
              var cursorX = 0, cursorY = 0;
              var wheelX = 0, wheelY = 0;
              var lClickX = 0, lClickY = 0;
              var w = 0, h = 0;
             // PWM値をpythonプログラムに渡す           
             function main() {
               var pwm1 = Math.round(1900 * cursorX/w) + 500;
               var pwm2 = Math.round(1900 * cursorY/h) + 500;
               var pwm3 = Math.round(1900 * wheelX / (w-17)) + 500;
               var pwm4 = Math.round(1900 * wheelY / (h-17)) + 500;
               var pwm5 = Math.round(1900 * lClickX/w) + 500;
               var pwm6 = Math.round(400 * lClickY/h) + 1700; // 1700 - 2100
               webiopi().callMacro("run_script", [pwm1,pwm2,pwm3,pwm4,pwm5,pwm6], callbackGetValue);
             }
             
              // Window上のマウス座標を取得する
              var mouseMove = function (e) {
                 if (e) {
                  cursorX = e.pageX - wheelX;
                  cursorY = e.pageY - wheelY;
                 } else {
                  cursorX = event.pageX;
                  cursorY = event.pageY;
                 }
              }
             document.addEventListener("mousemove",mouseMove);
             
              // ページスクロール量を取得する
              window.onscroll = function () {
               wheelX = document.documentElement.scrollLeft || document.body.scrollLeft;
               wheelY = document.documentElement.scrollTop || document.body.scrollTop;
            }
               
               
            // Windowサイズを取得する
             w = window.innerWidth;
             h = window.innerHeight;
            
            window.onresize = function (e) {
             w = window.innerWidth;
             h = window.innerHeight;
            }
           
            // 左クリック時にマウス座標取得
             document.onmousedown = function(event) {
               document.removeEventListener("mousemove",mouseMove);
               document.addEventListener("mousemove",onMouseMove);
             }
             var onMouseMove = function (e) {
                 if (e) {
                  lClickX = e.pageX - wheelX;
                  lClickY = e.pageY - wheelY;
                 } else {
                  lClickX = event.pageX;
                  lClickY = event.pageY;
                 }
             }
             document.onmouseup = function(event){
              document.addEventListener("mousemove",mouseMove);
              document.removeEventListener("mousemove",onMouseMove);
             }
           
             timer = setInterval(main, 550); // 550msごとにpythonプログラム実行
           })
           
           // ラズパイからの信号を止める
           function stopArm() {
               clearInterval(timer);
               webiopi().callMacro("run_script", [0, 0, 0, 0, 0, 0], callbackGetValue);
           }
           function callbackGetValue(macro, args, data) {
               console.log(macro);
               console.log(args);
               console.log(data);
           }
        </script>
        <style>
        <!--
           html {
               height: 100%;
           }
           
           body {
               height: 200%;
               margin: 0;
               width: 200%;
           }
        -->
        </style>
    </head>
    <body>
        <input type="button" value="stop" class="off" onClick="stopArm()">
        <p>再スタート:F5 or Ctr<</p> 
    </body>
</html>
import webiopi
import sys
import os
import time
import pigpio
sd = os.path.dirname(os.path.abspath(__file__))
sys.path.append(sd)
PIN1 = 21
PIN2 = 20
PIN3 = 16
PIN4 = 26
PIN5 = 19
PIN6 = 13
SLEEP_TIME = 0.05
pi = pigpio.pi() # Connect to local Pi.
@webiopi.macro
def run_script(pwm1, pwm2, pwm3, pwm4, pwm5, pwm6):
     pi.set_servo_pulsewidth(PIN1, pwm1)
     time.sleep(SLEEP_TIME)
     pi.set_servo_pulsewidth(PIN2, pwm2)
     time.sleep(SLEEP_TIME)
     pi.set_servo_pulsewidth(PIN3, pwm3)
     time.sleep(SLEEP_TIME)
     pi.set_servo_pulsewidth(PIN4, pwm4)
     time.sleep(SLEEP_TIME)
     pi.set_servo_pulsewidth(PIN5, pwm5)
     time.sleep(SLEEP_TIME)
     pi.set_servo_pulsewidth(PIN6, pwm6)
     time.sleep(SLEEP_TIME)
基本的なプログラムの流れは1軸制御の時と変わりません。異なる点はブラウザ上で読み取る値の数を増やしていることです。以下の6つを読み取るようにしています。
- マウスカーソルのx座標
- マウスカーソルのy座標
- スクロールのx方向の量
- スクロールのy方向の量
- 左クリック時のマウスカーソルのx座標
- 左クリック時のマウスカーソルのy座標
6つの値を読み取り、それぞれ6つのサーボへ信号を送るようになっています。左クリック時のマウスカーソルのy座標から取得しているpwm6は、他の5つよりも範囲を狭めて1700 ~ 2100となるようにしています(他の5つは500 ~ 2400の範囲)。pwm6のパルス信号を送るサーボモータは、ロボットアーム先端の物を挟むサーボモータであり、このサーボは180度全範囲動かすと非常にトルクを必要とすることから、可動範囲を狭めています。
Pythonコード(pwmservo.py)を書き換えた後は、端末上でwebiopiサービスの再起動を行うことを忘れないようにしてください。
配線
今回は6つのサーボモータを制御するので、6つ分の接続を行います。


ブラウザ上で動作確認
1軸の時と同様に、ブラウザへアクセスした直後にロボットアームが動き出します。。 注意点としては、6軸全部動くのでロボットアーム全体からすると大きく動く可能性があることです。 ロボットアーム周辺に置いてある物やテーブルに当たったり、テーブルから落下することがあるので、ブラウザへアクセス(または再読み込み)する前にロボットアームの周辺が片付いているか、ロボットアームが机から落ちないように固定されているか確認しましょう。
まとめ
Raspberry Piを使って6軸のロボットアームを動かせるようにしてみました。コントローラーとしてマウスを使えるように、ブラウザからマウス情報を読み取り、サーバ(Raspberry Pi内部)にあるPythonプログラムを実行するようにしています。