私的AI研究会 > 2軸カメラ台

2軸カメラ台を作る-WebIOPi応用2

事前準備

ハードウェア回路接続

● サーボモーターSG-90 とドライバー基板PCA-9865 を準備する。
● サーボモーターの電源は別途5Vを供給する。百均のUSB充電ケーブルを利用した。

 

WebIOPi によるブラウザでの動作確認

ドライバー基板の接続確認

pi@raspberrypi:~ $ sudo i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: 40 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: 70 -- -- -- -- -- -- --                         

40 に接続。

WebIOPi設定

pi@raspberrypi:~ $ sudo vi /etc/webiopi/config

   :

[SCRIPTS]
# Load custom scripts syntax :
# name = sourcefile
#   each sourcefile may have setup, loop and destroy functions and macros
#myscript = /home/pi/webiopi/examples/scripts/macros/script.py
#myscript = /home/pi/webiopi/callmacro_leds/script.py
#myscript = /home/pi/webiopi/bb/06/script.py
#myscript = /home/pi/webiopi/my06/script.py

   :

[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/callmacro_leds/
#doc-root = /home/pi/webiopi/bb/06/
#doc-root = /home/pi/webiopi/my06/

   :

[DEVICES]

pwm0 = PCA9685

WebIOPi を起動する

pi@raspberrypi:~ $ sudo service webiopi start

ブラウザから操作

● 表皮された画面から「Device Monitor」を選択する。
● 16チャネルのサーボモーターの操作スライダーが表皮され操作できることを確認。

Adafruit_PCA9685 を導入

pi@raspberrypi:~ $ sudo pip install adafruit-pca9685
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting adafruit-pca9685
   :
   :
Successfully installed Adafruit-GPIO-1.0.3 adafruit-pca9685-1.0.1 adafruit-pureio-1.0.1
pi@raspberrypi:~ $ sudo pip3 install Adafruit_PCA9685
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting Adafruit_PCA9685
   :
   :
Successfully installed Adafruit-GPIO-1.0.3 Adafruit-PCA9685-1.0.1 adafruit-pureio-1.0.1
pi@raspberrypi:~ $ pip freeze
Adafruit-GPIO==1.0.3
Adafruit-PCA9685==1.0.1
Adafruit-PureIO==1.0.1
   :

コンソールからテスト

pi@raspberrypi:~ $ python
Python 2.7.16 (default, Oct 10 2019, 22:02:15) 
[GCC 8.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import Adafruit_PCA9685
>>> pwm = Adafruit_PCA9685.PCA9685()
>>> pwm.set_pwm_freq(60)
>>> pwm.set_pwm(0, 0, 120)
>>> pwm.set_pwm(0, 0, 480)
>>> pwm.set_pwm(0, 0, 400)
>>> pwm.set_pwm(0, 0, 390)
>>> pwm.set_pwm(0, 0, 420)
>>> pwm.set_pwm(0, 0, 500)
>>> pwm.set_pwm(0, 0, 0)
>>> quit()
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ python3
Python 3.7.3 (default, Jul 25 2020, 13:03:44) 
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import Adafruit_PCA9685
>>> pwm = Adafruit_PCA9685.PCA9685()
>>> pwm.set_pwm_freq(60)
>>> pwm.set_pwm(0, 0, 500)
>>> pwm.set_pwm(0, 0, 300)
>>> pwm.set_pwm(0, 0, 390)
>>> pwm.set_pwm(0, 0, 550)
>>> pwm.set_pwm(0, 0, 0)
>>>            ←Ctrl+D
pi@raspberrypi:~ $ 

簡単なテスト

pi@raspberrypi:~/Programs $ vi servo.py
#! /usr/bin/env python

import time
import Adafruit_PCA9685

pwm = Adafruit_PCA9685.PCA9685()
pwm.set_pwm_freq(60)

while True:
    pwm.set_pwm(0, 0, 300)
    time.sleep(1)
    pwm.set_pwm(1, 0, 300)
    time.sleep(1)
    pwm.set_pwm(1, 0, 450)
    time.sleep(1)
    pwm.set_pwm(0, 0, 450)
    time.sleep(1)
    pwm.set_pwm(1, 0, 300)
    time.sleep(1)
    pwm.set_pwm(1, 0, 450)
    time.sleep(1)

テストプログラムの実行

pi@raspberrypi:~/Programs $ vi servo_pca9685.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import Adafruit_PCA9685
import time

class servo_Class:
    #ChannelはPCA9685のサーボモータの接続チャンネル
    #ZeroOffsetはサーボモータの基準位置調節用パラメータ
    def __init__(self, Channel, ZeroOffset):
        self.Channel = Channel
        self.ZeroOffset = ZeroOffset

        #Adafruit_PCA9685の初期化
        self.pwm = Adafruit_PCA9685.PCA9685(address=0x40)
        self.pwm.set_pwm_freq(60)

    # 角度設定
    def SetPos(self,pos):
        #PCA9685はパルスで角度を制御しており、パルス150~650が角度0~180に対応
        pulse = int((650-150)*pos/180+150+self.ZeroOffset)
        self.pwm.set_pwm(self.Channel, 0, pulse)

    # 終了処理
    def Cleanup(self):
        #サーボを90°にセット
        self.SetPos(90)

if __name__ == '__main__':
    Servo0 = servo_Class(Channel=0, ZeroOffset=0)
    Servo1 = servo_Class(Channel=1, ZeroOffset=0)

    try:
        while True:
            print ('30')
            Servo0.SetPos(30)
            Servo1.SetPos(30)
            time.sleep(1)

            print ('90')
            Servo0.SetPos(90)
            Servo1.SetPos(90)
            time.sleep(1)

            print ('150')
            Servo0.SetPos(150)
            Servo1.SetPos(150)
            time.sleep(1)

            print ('90')
            Servo0.SetPos(90)
            Servo1.SetPos(90)
            time.sleep(1)

    except KeyboardInterrupt:
        print("\nCtl+C")
    except Exception as e:
        print(str(e))
    finally:
        Servo0.Cleanup()
        Servo1.Cleanup()
        print("\nexit program")

ブラウザから操作する

事前準備

MJPG-streamer Tips

WebIOPi Tips

WebIOPi デバッグ出力機能

WebIOPi 環境に組み込む

ハードウェア回路接続

● 前節の回路を使用する。
● サーボモーターの電源は別途5Vを供給する。
● カメラと上記の回路以外のハードウェアは外しておく。

 

ソフトウェア作成

pi@raspberrypi:~ $ cd /home/pi/webiopi/my08
pi@raspberrypi:~/webiopi/my08 $ ls
index.html  javascript.js  js  script.py  styles.css
pi@raspberrypi:~/webiopi/my08 $ vi index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<title>6 - クローラーの操作 (2軸コントロールカメラつき)</title>
<!-- jQuery UI -->
<link rel="stylesheet" href="js/jquery-ui.min.css">
<script src="js/jquery-1.12.4.min.js"></script>
<script src="js/jquery-ui.min.js"></script>
<script src="js/jquery.ui.touch-punch.js"></script>

<!-- WebIOPi -->
<script src="js/require.js"></script>
<script>
require(["/webiopi.js", "javascript.js"], function(){
    webiopi().ready( initialize_webiopi );
});
</script>
</head>
<body>

<div align="center">
    <div id="box">
        <span id="touchArea"><canvas id="canvas" oncontextmenu="return false;"></canvas></span>
        <span id="slider1"></span>
    </div>
    <div id="slider0"></div>
</div>

<div align="center">
<p>【タッチ操作】→ タッチしている間だけモーターは回転し、タッチを離すと静止</p>

<p>【マウス操作】→ マウスをクリックした位置により、モーターが回転を開始。中央付近のクリックでモーター停止。</p>
</dev>

</body>
</html>
pi@raspberrypi:~/webiopi/my08 $ vi stile.css

#touchArea{
    -ms-touch-action: none;
    touch-action: none;
}

#slider1 {
  display: inline-block;
}

.ui-slider .ui-slider-handle {
    width: 30px;
    height: 30px;
}
pi@raspberrypi:~/webiopi/my08 $ vi javascript.js

// タッチのサポート状況のチェック用変数
var support = {
    pointer: window.navigator.pointerEnabled,
    mspointer: window.navigator.msPointerEnabled,
    touch: 'ontouchstart' in window
};

// タッチの場合わけ。pointer系:IE11以降、MSPointer系:IE10、touch系:android、iPhone、iPad
var touchStart = support.pointer ? 'pointerdown' :
                 support.mspointer ? 'MSPointerDown' : 'touchstart';
var touchMove =  support.pointer ? 'pointermove' :
                 support.mspointer ? 'MSPointerMove' : 'touchmove';
var touchEnd =   support.pointer ? 'pointerup' :
                 support.mspointer ? 'MSPointerUp' : 'touchend';

function initialize_webiopi(){
    // webiopiの準備が終わってからstyles.cssを適用する
    applyCustomCss('styles.css');

    mCanvas = document.getElementById("canvas");
    mCtx = mCanvas.getContext('2d');

    resize_canvas();

    // タッチエリアの設定
    var touchArea = $("#touchArea")[0];

    // タッチイベントのイベントリスナーの登録
    touchArea.addEventListener(touchStart, touchEvent, false);
    touchArea.addEventListener(touchMove, touchEvent, false);
    touchArea.addEventListener(touchEnd, touchEndEvent, false);

    // クリックイベントのイベントリスナーの登録
    touchArea.addEventListener("click", clickEvent, false);

    // iOSでcanvas上のスクロールを抑制
    document.addEventListener("touchstart", preventScroll, false);
    document.addEventListener("touchmove", preventScroll, false);
    document.addEventListener("touchend", preventScroll, false);

    // Firefoxで画面の回転を検出
    var mqOrientation = window.matchMedia("(orientation: portrait)");
    mqOrientation.addListener(function() {
        resize_canvas();
    });

    // ウインドウサイズの変更を検出
    window.addEventListener('resize', function (event) {
        resize_canvas();
    });

    // GPIOの状態を監視しない
    webiopi().refreshGPIO(false);
}

// iOSで画面の回転を検出
window.onorientationchange = function()
{
    var iR = Math.abs( window.orientation );
    if ( iR == 0 || iR == 90 ){
        resize_canvas();
    }
}

// スライダの最小値、最大値、刻み幅、初期値
var sliderMin = 0;
var sliderMax = 20;
var sliderStep = 1;
var sliderValue = sliderMax/2;

$(function() {
    var sliderHandler1 = function(e, ui){
        var ratio = ui.value/sliderMax;
        // サーボの回転の向きを逆にしたい場合次の行を無効に
        //ratio = 1.0 - ratio;
        webiopi().callMacro("setPCA9685PWM", [1, ratio, commandID++]);
    };
    var sliderHandler0 = function(e, ui){
        var ratio = ui.value/sliderMax;
        // サーボの回転の向きを逆にしたい場合次の行を無効に
        //ratio = 1.0 - ratio;
        webiopi().callMacro("setPCA9685PWM", [0, ratio, commandID++]);
    };

    $( "#slider1" ).slider({
        orientation: "vertical",
        min: sliderMin,
        max: sliderMax,
        step: sliderStep,
        value: sliderValue,
        change: sliderHandler1,
        slide: sliderHandler1
    });
    $( "#slider0" ).slider({
        orientation: "horizontal",
        min: sliderMin,
        max: sliderMax,
        step: sliderStep,
        value: sliderValue,
        change: sliderHandler0,
        slide: sliderHandler0
    });
});

// 前に送信したデューティー比を覚えておく
var rate25Prev = 0;
var rate24Prev = 0;
var rate23Prev = 0;
var rate22Prev = 0;

// デューティー比がth (0.0~1.0) 以上変化した時のみ値を送信
var th = 0.1;
// モーターの最大速度 (0.0~1.0)。モーターを保護する意味で1.0にはしない方が良い
var maxSpeed = 0.7;
// 命令送信ごとに増加するIDを作成(iOSのSafariでPOSTがキャッシュされることの対策)
var commandID = 0;

var mCount = 0;
var mCanvas;
var mCtx;
var mImg1;
var mImg2;
var mImgArrow;
var mWidth = 640;
var mHeight = 480;

var host = location.host;
var hostname = host.split(":")[0];
var port= 9000;
var URL1 = 'http://' + hostname + ':' + port + '/?action=snapshot';
//var URL2 = 'http://' + hostname + ':8000/bb/06/img/CrawlerControllerTrans.png';
var URL2 = 'http://' + hostname + ':8000/img/CrawlerControllerTrans.png';

var mTouchWidth;
var mTouchHeight;
var mTouchOffsetTop;
var mTouchOffsetLeft;

function imageSetup(){
    mImg1 = new Image();
    mImg2 = new Image();
    mImgArrow = new Image();

    mImg1.src = URL1 +'&'+(mCount++);

    mImgArrow.src = URL2;

    mImg1.onload = function() {
        mImg2.src = URL1 + '&' + (mCount++);
        mCtx.drawImage(mImg1, 0, 0, mWidth, mHeight);
        mCtx.drawImage(mImgArrow, 0, 0, mWidth, mHeight);
    };

    mImg2.onload = function() {
        mImg1.src =URL1 + '&' + (mCount++);
        mCtx.drawImage(mImg2, 0, 0, mWidth, mHeight);
        mCtx.drawImage(mImgArrow, 0, 0, mWidth, mHeight);
    };
}

function touchEvent(e){
    e.preventDefault();

    // タッチ中のイベントのみ捕捉(IE)
    if(support.pointer || support.mspointer){
        if(e.pointerType != 'touch' && e.pointerType != 2){
            return;
        }
    }
    var touch = (support.pointer || support.mspointer) ? e : e.touches[0];

    // エリア外のタッチを無視
    if(touch.pageX < mTouchOffsetLeft ||
       touch.pageX >= mTouchOffsetLeft + mTouchWidth ||
       touch.pageY < mTouchOffsetTop ||
       touch.pageY >= mTouchOffsetTop + mTouchHeight){

        return;
    }

    if(touch.pageX < mTouchOffsetLeft + mTouchWidth/3){ // 左旋回
       var rate = maxSpeed*(mTouchOffsetLeft + mTouchWidth/3-touch.pageX)/(mTouchWidth/3);

        // 前回送信時と値が大きく違うときのみ送信
        if(Math.abs(rate-rate24Prev)>th || Math.abs(rate-rate23Prev)>th){
            webiopi().callMacro("pwm4Write", [0, rate, rate, 0, commandID++]);
            rate25Prev = 0;
            rate24Prev = rate;
            rate23Prev = rate;
            rate22Prev = 0;
        }
    }else if(touch.pageX < mTouchOffsetLeft + 2*mTouchWidth/3){ // 前後移動
        // 左右の車輪の速さの違いの補正
        var modL = (1.2-0.8)*(touch.pageX - mTouchOffsetLeft -mTouchWidth/3)/(mTouchWidth/3) + 0.8;
        var modR = (0.8-1.2)*(touch.pageX - mTouchOffsetLeft - mTouchWidth/3)/(mTouchWidth/3) + 1.2;

        if(touch.pageY < mTouchOffsetTop + mTouchHeight/2){
            var rate = maxSpeed*(mTouchHeight/2 + mTouchOffsetTop - touch.pageY)/(mTouchHeight/2);
            modL *= rate;
            modR *= rate;

            if(modL > 1.0){ modL = 1.0; }
            if(modR > 1.0){ modR = 1.0; }

            // 前回送信時と値が大きく違うときのみ送信
            if(Math.abs(modL-rate25Prev)>th || Math.abs(modR-rate23Prev)>th){
                webiopi().callMacro("pwm4Write", [modL, 0, modR, 0, commandID++]);
                rate25Prev = modL;
                rate24Prev = 0;
                rate23Prev = modR;
                rate22Prev = 0;
            }
        }else{
            var rate = maxSpeed*(touch.pageY - mTouchOffsetTop - mTouchHeight/2)/(mTouchHeight/2);
            modL *= rate;
            modR *= rate;

            if(modL > 1.0){ modL = 1.0; }
            if(modR > 1.0){ modR = 1.0; }

            // 前回送信時と値が大きく違うときのみ送信
            if(Math.abs(modL-rate24Prev)>th || Math.abs(modR-rate22Prev)>th){
                webiopi().callMacro("pwm4Write", [0, modL, 0, modR, commandID++]);
                rate25Prev = 0;
                rate24Prev = modL;
                rate23Prev = 0;
                rate22Prev = modR;
            }
        }

    }else{ // 右旋回
        var rate = maxSpeed*(touch.pageX - mTouchOffsetLeft - 2*mTouchWidth/3)/(mTouchWidth/3);

        // 前回送信時と値が大きく違うときのみ送信
        if(Math.abs(rate-rate25Prev)>th || Math.abs(rate-rate22Prev)>th){
            webiopi().callMacro("pwm4Write", [rate, 0, 0, rate, commandID++]);
            rate25Prev = rate;
            rate24Prev = 0;
            rate23Prev = 0;
            rate22Prev = rate;
        }
    }

}

// タッチ終了時のイベントリスナー
function touchEndEvent(e){
    e.preventDefault();

    webiopi().callMacro("pwm4Write", [0, 0, 0, 0, commandID++]);
    rate25Prev = 0;
    rate24Prev = 0;
    rate23Prev = 0;
    rate22Prev = 0;
}

// クリック時のイベントリスナー(主にPC用)
function clickEvent(e){
    e.preventDefault();

    // タッチによるクリックは無視(IE)
    if(support.pointer || support.mspointer){
        if(e.pointerType == 'touch' || e.pointerType == 2){
            return;
        }
    }

    // エリア外のクリックを無視
    if(e.pageX < mTouchOffsetLeft ||
       e.pageX >= mTouchOffsetLeft + mTouchWidth ||
       e.pageY < mTouchOffsetTop ||
       e.pageY >= mTouchOffsetTop + mTouchHeight){

        return;
    }

    if(e.pageX < mTouchOffsetLeft + mTouchWidth/3){ // 左旋回
        var rate = maxSpeed*(mTouchOffsetLeft + mTouchWidth/3 - e.pageX)/(mTouchWidth/3);

        webiopi().callMacro("pwm4Write", [0, rate, rate, 0, commandID++]);
        rate25Prev = 0;
        rate24Prev = rate;
        rate23Prev = rate;
        rate22Prev = 0;
    }else if(e.pageX < mTouchOffsetLeft + 2*mTouchWidth/3){ // 前後移動
        // 左右の車輪の速さの違いの補正
        var modL = (1.2-0.8)*(e.pageX - mTouchOffsetLeft -mTouchWidth/3)/(mTouchWidth/3) + 0.8;
        var modR = (0.8-1.2)*(e.pageX - mTouchOffsetLeft - mTouchWidth/3)/(mTouchWidth/3) + 1.2;

        if(e.pageY >= mTouchOffsetTop + 2*mTouchHeight/5 && e.pageY < mTouchOffsetTop + 3*mTouchHeight/5){
            webiopi().callMacro("pwm4Write", [0, 0, 0, 0, commandID++]);
            rate25Prev = 0;
            rate24Prev = 0;
            rate23Prev = 0;
            rate22Prev = 0;
        }else if(e.pageY < mTouchOffsetTop + mTouchHeight/2){
            var rate = maxSpeed*(mTouchOffsetTop + mTouchHeight/2 - e.pageY)/(mTouchHeight/2);
            modL *= rate;
            modR *= rate;

            if(modL > 1.0){ modL = 1.0; }
            if(modR > 1.0){ modR = 1.0; }

            webiopi().callMacro("pwm4Write", [modL, 0, modR, 0, commandID++]);
            rate25Prev = modL;
            rate24Prev = 0;
            rate23Prev = modR;
            rate22Prev = 0;
        }else{
            var rate = maxSpeed*(e.pageY - mTouchOffsetTop - mTouchHeight/2)/(mTouchHeight/2);
            modL *= rate;
            modR *= rate;

            if(modL > 1.0){ modL = 1.0; }
            if(modR > 1.0){ modR = 1.0; }

            webiopi().callMacro("pwm4Write", [0, modL, 0, modR, commandID++]);
            rate25Prev = 0;
            rate24Prev = modL;
            rate23Prev = 0;
            rate22Prev = modR;
        }

    }else{ // 右旋回
        var rate = maxSpeed*(e.pageX - mTouchOffsetLeft - 2*mTouchWidth/3)/(mTouchWidth/3);

        webiopi().callMacro("pwm4Write", [rate, 0, 0, rate, commandID++]);
        rate25Prev = rate;
        rate24Prev = 0;
        rate23Prev = 0;
        rate22Prev = rate;
    }

}

function resize_canvas(){

    if($(window).width() < 4*$(window).height()/3){
        isPortrait = true;
    }else{
        isPortrait = false;
    }

    if(isPortrait){
        mTouchWidth = 0.9*$(window).width();
        mTouchHeight = 3*mTouchWidth/4;
    }else{
        mTouchHeight = 0.9*$(window).height();
        mTouchWidth = 4*mTouchHeight/3;
    }

    mWidth = mTouchWidth;
    mHeight = mTouchHeight;

    mCanvas.width = mWidth;
    mCanvas.height = mHeight

    mTouchOffsetLeft = $("#canvas").offset().left;
    mTouchOffsetTop = $("#canvas").offset().top;

    $( "#slider1" ).height(mHeight);
    $( "#slider0" ).width(mWidth);

    imageSetup();
}

function applyCustomCss(custom_css){
    var head = document.getElementsByTagName('head')[0];
    var style = document.createElement('link');
    style.rel = "stylesheet";
    style.type = 'text/css';
    style.href = custom_css;
    head.appendChild(style);
}
pi@raspberrypi:~/webiopi/my08 $ vi script.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import webiopi
import time
import subprocess
import Adafruit_PCA9685

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

# GPIOライブラリの取得
GPIO = webiopi.GPIO

# PCA9685サーボモーターコントロール
class servo_Class:
    #ChannelはPCA9685のサーボモータの接続チャンネル
    #ZeroOffsetはサーボモータの基準位置調節用パラメータ
    def __init__(self, Channel, ZeroOffset):
        self.Channel = Channel
        self.ZeroOffset = ZeroOffset

        #Adafruit_PCA9685の初期化
        self.pwm = Adafruit_PCA9685.PCA9685(address=0x40)
        self.pwm.set_pwm_freq(60)

    # 角度設定
    def SetPos(self,pos):
        #PCA9685はパルスで角度を制御しており、パルス150~650が角度0~180に対応
        pulse = int((650-150)*pos/180+150+self.ZeroOffset)
        self.pwm.set_pwm(self.Channel, 0, pulse)

    # 終了処理
    def Cleanup(self):
        #サーボを90°にセット
        self.SetPos(90)


Servo0 = servo_Class(Channel=0, ZeroOffset=0)    # Pan
Servo1 = servo_Class(Channel=1, ZeroOffset=0)    # Tilt

PWM1 = 25       # R AIN1
PWM2 = 24       #   AIN2
PWM3 = 23       # L BIN1
PWM4 = 22       #   BIN2

LED1 = 21       # SW
LED2 = 12       # R LED
LED3 = 13       # L LED

led2_mode = 0   # 右LED 0 = OFF, 1 = ON
led3_mode = 0   # 左LED 0 = OFF, 1 = ON

# WebIOPiの起動時に呼ばれる関数
def setup():
    webiopi.debug("Script with macros - Setup")
    subprocess.call("aplay -q /home/pi/Voice/webiopi-start.wav", shell=True)
# GPIOのセットアップ
    GPIO.setFunction(PWM1, GPIO.PWM)
    GPIO.setFunction(PWM2, GPIO.PWM)
    GPIO.setFunction(PWM3, GPIO.PWM)
    GPIO.setFunction(PWM4, GPIO.PWM)
    # 初期のデューティー比を0%に(静止状態)
    GPIO.pwmWrite(PWM1, 0)
    GPIO.pwmWrite(PWM2, 0)
    GPIO.pwmWrite(PWM3, 0)
    GPIO.pwmWrite(PWM4, 0)
    
    # LED
    GPIO.setFunction(LED1, GPIO.OUT)
    GPIO.setFunction(LED2, GPIO.OUT)
    GPIO.setFunction(LED3, GPIO.OUT)
    # LED 初期設定
    GPIO.digitalWrite(LED1, 1)
    GPIO.digitalWrite(LED2, 0)
    GPIO.digitalWrite(LED3, 0)

# WebIOPiにより繰り返される関数
def loop():
    global led2_mode, led3_mode

    for num in range(10):
        if led2_mode >= 0 and led3_mode >= 0:
            if led2_mode == 0:
                GPIO.digitalWrite(LED2, 0)
            else:
                GPIO.digitalWrite(LED2, 1)
            if led3_mode == 0:
                GPIO.digitalWrite(LED3, 0)
            else:
                GPIO.digitalWrite(LED3, 1)

        webiopi.sleep(0.25)

        if led2_mode >= 0 and led3_mode >= 0:
            if led2_mode == 1:
                GPIO.digitalWrite(LED2, 1)
            else:
                GPIO.digitalWrite(LED2, 0)
            if led3_mode == 1:
                GPIO.digitalWrite(LED3, 1)
            else:
                GPIO.digitalWrite(LED3, 0)

        webiopi.sleep(0.25)


# WebIOPi終了時に呼ばれる関数
def destroy():
    global led2_mode, led3_mode

    webiopi.debug("Script with macros - Destroy")
    subprocess.call("aplay -q /home/pi/Voice/webiopi-stop.wav", shell=True)
    # GPIO関数のリセット(入力にセットすることで行う)
    GPIO.setFunction(PWM1, GPIO.IN)
    GPIO.setFunction(PWM2, GPIO.IN)
    GPIO.setFunction(PWM3, GPIO.IN)
    GPIO.setFunction(PWM4, GPIO.IN)
    
    led2_mode = -1
    led3_mode = -1
    GPIO.setFunction(LED1, GPIO.IN)
    GPIO.setFunction(LED2, GPIO.IN)
    GPIO.setFunction(LED3, GPIO.IN)
    
    # カメラを中央に戻す
    Servo0.Cleanup()
    Servo1.Cleanup()


# 4つのPWMにデューティー比をまとめてセットするためのマクロ
# commandIDは、iOSのSafariでPOSTがキャッシュされることへの対策
@webiopi.macro
def pwm4Write(duty1, duty2, duty3, duty4, commandID):
    global led2_mode, led3_mode
    
    GPIO.pwmWrite(PWM1, float(duty1))
    GPIO.pwmWrite(PWM2, float(duty2))
    GPIO.pwmWrite(PWM3, float(duty3))
    GPIO.pwmWrite(PWM4, float(duty4))

    d1 = int(float(duty1) * 100.0)
    d2 = int(float(duty2) * 100.0)
    d3 = int(float(duty3) * 100.0)
    d4 = int(float(duty4) * 100.0)
    if d1 == 0 and d2 == 0:
        led2_mode = 0
        led3_mode = 0
        GPIO.digitalWrite(LED2, 0)
        GPIO.digitalWrite(LED3, 0)
    else:
        if d1 > 0 and d2 == 0:              # R 前進
            led2_mode = 1
            GPIO.digitalWrite(LED2, 1)
        elif d1 == 0 and d2 > 0:            # R 後退
            led2_mode = 2
        if d3 > 0 and d4 == 0:              # L 前進
            led3_mode = 1
            GPIO.digitalWrite(LED3, 1)
        elif d3 == 0 and d4 > 0:            # L 後退
            led3_mode = 2
    
    wav_file = "aplay -q /home/pi/Voice/"
    wav_mode = 0
    if led2_mode == 0 and led3_mode == 0:
        wav_file += "stop.wav"
        wav_mode = 1
    elif led2_mode == 1 and led3_mode == 1:
        wav_file += "foward.wav"
        wav_mode = 1
    elif led2_mode == 2 and led3_mode == 2:
        wav_file += "back.wav"
        wav_mode = 1
    elif led2_mode == 1 and led3_mode == 2:
        wav_file += "right.wav"
        wav_mode = 1
    elif led2_mode == 2 and led3_mode == 1:
        wav_file += "left.wav"
        wav_mode = 1
    
    if wav_mode == 1:
        subprocess.call(wav_file, shell=True)

    webiopi.debug("led2_mode = " + str(led2_mode))
    webiopi.debug("led3_mode = " + str(led3_mode))

# 2軸のサーボモーターコントロールのためのマクロ
#  servoID: 0 = Horizontal, 1 = Vertical
#  duty: 0.0 ~ 0.5 ~ 1.0
#  degr:  40 ~  90 ~ 140  (角度)
@webiopi.macro
def setPCA9685PWM(servoID, duty, commandID):
    degr = (140-40)*float(duty) + 40
#    webiopi.debug("servoID = " + str(servoID) + "  duty = " + str(float(duty)) + "  degr = " + str(degr))
    if int(servoID) == 0:
#        webiopi.debug(">> Pan degr = " + str(degr))
        Servo0.SetPos(degr)
    elif int(servoID) == 1:
#        webiopi.debug(">> Tilt degr = " + str(degr))
        Servo1.SetPos(degr)
pi@raspberrypi:~ $ sudo vi /etc/webiopi/config

   :
[SCRIPTS]
myscript = /home/pi/webiopi/my08/script.py

   :
[HTTP]
doc-root = /home/pi/webiopi/my08/

   :
[DEVICES]
#pwm0 = #PCA9685

実行

 

WebIOPi 組み込みドライバを使用する

スクリプトファイルを変更

pi@raspberrypi:~/webiopi/my08 $ cp script.py script_adafruit.py
pi@raspberrypi:~/webiopi/my08 $ vi script.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import webiopi
from webiopi import deviceInstance
import time
import subprocess

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

# GPIOライブラリの取得
GPIO = webiopi.GPIO

# PCA9685ライブラリの取得
pwm0 = deviceInstance("pwm0")

PWM1 = 25       # R AIN1
PWM2 = 24       #   AIN2
PWM3 = 23       # L BIN1
PWM4 = 22       #   BIN2

LED1 = 21       # SW
LED2 = 12       # R LED
LED3 = 13       # L LED

led2_mode = 0   # 右LED 0 = OFF, 1 = ON
led3_mode = 0   # 左LED 0 = OFF, 1 = ON

# WebIOPiの起動時に呼ばれる関数
def setup():
    webiopi.debug("Script with macros - Setup")
    subprocess.call("aplay -q /home/pi/Voice/webiopi-start.wav", shell=True)
# GPIOのセットアップ
    GPIO.setFunction(PWM1, GPIO.PWM)
    GPIO.setFunction(PWM2, GPIO.PWM)
    GPIO.setFunction(PWM3, GPIO.PWM)
    GPIO.setFunction(PWM4, GPIO.PWM)
    # 初期のデューティー比を0%に(静止状態)
    GPIO.pwmWrite(PWM1, 0)
    GPIO.pwmWrite(PWM2, 0)
    GPIO.pwmWrite(PWM3, 0)
    GPIO.pwmWrite(PWM4, 0)
    
    # LED
    GPIO.setFunction(LED1, GPIO.OUT)
    GPIO.setFunction(LED2, GPIO.OUT)
    GPIO.setFunction(LED3, GPIO.OUT)
    # LED 初期設定
    GPIO.digitalWrite(LED1, 1)
    GPIO.digitalWrite(LED2, 0)
    GPIO.digitalWrite(LED3, 0)

# WebIOPiにより繰り返される関数
def loop():
    global led2_mode, led3_mode

    for num in range(10):
        if led2_mode >= 0 and led3_mode >= 0:
            if led2_mode == 0:
                GPIO.digitalWrite(LED2, 0)
            else:
                GPIO.digitalWrite(LED2, 1)
            if led3_mode == 0:
                GPIO.digitalWrite(LED3, 0)
            else:
                GPIO.digitalWrite(LED3, 1)

        webiopi.sleep(0.25)

        if led2_mode >= 0 and led3_mode >= 0:
            if led2_mode == 1:
                GPIO.digitalWrite(LED2, 1)
            else:
                GPIO.digitalWrite(LED2, 0)
            if led3_mode == 1:
                GPIO.digitalWrite(LED3, 1)
            else:
                GPIO.digitalWrite(LED3, 0)

        webiopi.sleep(0.25)


# WebIOPi終了時に呼ばれる関数
def destroy():
    global led2_mode, led3_mode

    webiopi.debug("Script with macros - Destroy")
    subprocess.call("aplay -q /home/pi/Voice/webiopi-stop.wav", shell=True)
    # GPIO関数のリセット(入力にセットすることで行う)
    GPIO.setFunction(PWM1, GPIO.IN)
    GPIO.setFunction(PWM2, GPIO.IN)
    GPIO.setFunction(PWM3, GPIO.IN)
    GPIO.setFunction(PWM4, GPIO.IN)
    
    led2_mode = -1
    led3_mode = -1
    GPIO.setFunction(LED1, GPIO.IN)
    GPIO.setFunction(LED2, GPIO.IN)
    GPIO.setFunction(LED3, GPIO.IN)
    
    # カメラを中央に戻す
    pwm0.pwmWriteAngle(0, 0)
    time.sleep(1)
    pwm0.pwmWriteAngle(1, 0)

# 4つのPWMにデューティー比をまとめてセットするためのマクロ
# commandIDは、iOSのSafariでPOSTがキャッシュされることへの対策
@webiopi.macro
def pwm4Write(duty1, duty2, duty3, duty4, commandID):
    global led2_mode, led3_mode
    
    GPIO.pwmWrite(PWM1, float(duty1))
    GPIO.pwmWrite(PWM2, float(duty2))
    GPIO.pwmWrite(PWM3, float(duty3))
    GPIO.pwmWrite(PWM4, float(duty4))

    d1 = int(float(duty1) * 100.0)
    d2 = int(float(duty2) * 100.0)
    d3 = int(float(duty3) * 100.0)
    d4 = int(float(duty4) * 100.0)
    if d1 == 0 and d2 == 0:
        led2_mode = 0
        led3_mode = 0
        GPIO.digitalWrite(LED2, 0)
        GPIO.digitalWrite(LED3, 0)
    else:
        if d1 > 0 and d2 == 0:              # R 前進
            led2_mode = 1
            GPIO.digitalWrite(LED2, 1)
        elif d1 == 0 and d2 > 0:            # R 後退
            led2_mode = 2
        if d3 > 0 and d4 == 0:              # L 前進
            led3_mode = 1
            GPIO.digitalWrite(LED3, 1)
        elif d3 == 0 and d4 > 0:            # L 後退
            led3_mode = 2
    
    wav_file = "aplay -q /home/pi/Voice/"
    wav_mode = 0
    if led2_mode == 0 and led3_mode == 0:
        wav_file += "stop.wav"
        wav_mode = 1
    elif led2_mode == 1 and led3_mode == 1:
        wav_file += "foward.wav"
        wav_mode = 1
    elif led2_mode == 2 and led3_mode == 2:
        wav_file += "back.wav"
        wav_mode = 1
    elif led2_mode == 1 and led3_mode == 2:
        wav_file += "right.wav"
        wav_mode = 1
    elif led2_mode == 2 and led3_mode == 1:
        wav_file += "left.wav"
        wav_mode = 1
    
    if wav_mode == 1:
        subprocess.call(wav_file, shell=True)

    webiopi.debug("led2_mode = " + str(led2_mode))
    webiopi.debug("led3_mode = " + str(led3_mode))

# 2軸のサーボモーターコントロールのためのマクロ
#  servoID: 0 = Horizontal, 1 = Vertical
#  duty: 0.0 ~ 0.5 ~ 1.0
#  degr: -50 ~  0  ~  50  (角度)
@webiopi.macro
def setPCA9685PWM(servoID, duty, commandID):
    degr = int(100*float(duty) - 50)
    if (int(servoID) == 0):
        degr = -degr
    
    pwm0.pwmWriteAngle(int(servoID), degr)
#    webiopi.debug("servoID = " + str(servoID) + "  duty = " + str(float(duty)) + "  degr = " + str(degr))
pi@raspberrypi:~ $ sudo vi /etc/webiopi/config

   :
[SCRIPTS]
myscript = /home/pi/webiopi/my08/script.py

   :
[HTTP]
doc-root = /home/pi/webiopi/my08/

   :
[DEVICES]
pwm0 = PCA9685

実行

キャタピラーロボットに2軸カメラ台を取り付ける

2軸カメラ台を搭載できるBOX作成

ブラウザからGPIO を操作3-WebIOPi応用 で完成した喋るキャタピラーロボットを解体して2軸カメラ台を搭載する。

RaspberryPi基板のある上部の箱を解体

● 上部の箱を半分に切断、逆さにしてスピーカー・オーディオ基板・電池ボックスを組み込む。

上部に箱を追加

● 箱を2段にしてRaspberryPi基板・カメラ台を取り付けその上にサーボドライバー基板・を搭載。
● IPアドレスを表示するLCD基板とシャットダウンスイッチを組み込む。

駆動部の上に載せて完成

● 駆動部は分離できて、本体部分のみで動作可能なのでこのままプログラム開発に利用できる。

携帯用のBOXの改良

● 蓋と本体の留め具と取っ手を付けて完成。

 

自動起動設定をする。

シャットダウンSW

IPアドレス表示

MJPG-streamer の起動ファイル

自動起動ファイル

 

参考資料

参考サイト

 

Last-modified: 2020-12-09 (水) 07:23:20