Raspberry Piのライブ配信Web UIをBootstrapで整える

Screenshot_2015-12-05-12-50-18

前回でとりあえずブラウザでライブ映像を観ることができるようになったので、その後、Bootstrapで簡単にですがWebUIを整えました。それに伴い、カメラの操作方法を変えたり、好きなタイミングで画像を撮影できるボタンを追加したりしたので、今回はそのお話です。

WebIOPiの利用を前提として、現在のディレクトリ構成はこんな感じになっています。

cat-monitoring
├── css
│   ├── bootstrap.min.css
│   └── styles.css
├── fonts
│   ├── glyphicons-halflings-regular.eot
│   ├── glyphicons-halflings-regular.svg
│   ├── glyphicons-halflings-regular.ttf
│   ├── glyphicons-halflings-regular.woff
│   └── glyphicons-halflings-regular.woff2
├── html
│   └── index.html
├── img
├── js
│   ├── bootstrap.min.js
│   ├── cat-monitoring.js
│   ├── jquery-1.11.3.min.js
│   └── require.js
└── python
    └── servo-control.py

WebIOPiの設定ファイル”/etc/webiopi/config”では、以下のように指定している状態です。

myscript = /home/pi/cat-monitoring/python/servo-control.py
doc-root = /home/pi/cat-monitoring/html

それから、motionの設定ファイル”/etc/motion/motion.conf”で

target_dir /home/pi/cat-monitoring/img

のように指定しているので、撮影時の写真はここで作成している”img”ディレクトリに保存されるようにしています。

“fonts”ディレクトリの中身と、bootstrap.xxxになっているファイルはBootstrapをダウンロードしたときの中身をそのまま持ってきているだけなので、以下は自作する必要のあるファイルだけ載せていきます。あ、JqueryRequire.jsのダウンロードもお忘れなく(未だにrequire.jsが本当に必要かどうかよくわからずに使っていますが)。

まずメインの”html/index.html”から。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Cats Monitoring</title>
  <link href="../css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
  <div class="container">

   <div class="panel panel-default">

      <div class="panel-heading">
        <h1 class="page-title">Cats Monitoring</h1>
      </div>

      <div class="panel-body">
        <div class="row">
          <div class="col-xs-offset-1 col-xs-10">
            <img src="http://(Raspberry Piのアドレス):8081/" class="img-responsive">
          </div>
        </div>

        <div class="row">
          <div class="col-xs-offset-4 col-xs-4">
            <button class="btn btn-info" id="upBtn">↑</button>
          </div>
        </div>

        <div class="row">
          <div class="col-xs-offset-1 col-xs-3">
            <button class="btn btn-info" id="leftBtn">←</button>
          </div>
          <div class="col-xs-4">
            <button class="btn btn-primary" id="homeBtn">HOME</button>
          </div>
          <div class="col-xs-3 col-xs-pull-1">
            <button class="btn btn-info" id="rightBtn">→</button>
          </div>
        </div>

        <div class="row">
          <div class="col-xs-offset-4 col-xs-4">
            <button class="btn btn-info" id="downBtn">↓</button>
          </div>
        </div>

        <div class="row">
          <div class="col-xs-offset-4 col-xs-4">
            <button class="btn btn-warning" id="shotBtn">Shot</button>
          </div>
        </div>
      </div>

      <div class="panel-footer text-center">
        <p>Made for Pii &amp; Fuu</p>
      </div>

    </div>

  </div>
  <script src="../js/jquery-1.11.3.min.js"></script>
  <script src="../js/bootstrap.min.js"></script>
  <script src="../js/require.js"></script>
  <script>
    require(["/webiopi.js", "../js/cat-monitoring.js"], function(){
      webiopi().ready(initialize_webiopi);
    });
  </script>
</body>
</html>

基本的に自分がモニタリングでメインに使う端末(FREETEL Priori 3 LTE)に合わせて調整しているので、必要に応じて”col-xs-“とか”col-xs-offset-“, “col-xs-pull-“とかの値を調整してもらえればと思います。

続いて”css/styles.css”です。

button {
  display: block;
  margin: 2px;
  height: 45px;
  width: 65px;
}

これはボタンのサイズ関係を調整しているだけです。ボタンの色については、”index.html”内の各ボタンで適用している、Bootstrapの組み込みクラス(”btn-info”とか”btn-warning”)で変更しています。

 続いて”js/cat-monitoring.js”です。

var LEVEL_MAX = 10;
var LEVEL_MIN =  0;
var currentLevelPan  = 5;
var currentLevelTilt = 5;
var commandID = 0;

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);
}

function initialize_webiopi(){
  applyCustomCss('../css/styles.css')
  webiopi().refreshGPIO(false);
}

$(function() {

  $("#homeBtn").click(function(){
    console.log("JS:Home")
    currentLevelPan  = 5;
    currentLevelTilt = 5;
    webiopi().callMacro("setHwPWMforPan", [currentLevelPan/LEVEL_MAX, commandID++]);
    var tilt = setInterval(function(){
      webiopi().callMacro("setHwPWMforTilt", [currentLevelTilt/LEVEL_MAX, commandID++]);
      clearInterval(tilt);
    }, 100);
  });

  $("#upBtn").click(function(){
    if(currentLevelTilt < LEVEL_MAX){
      currentLevelTilt += 1;
      webiopi().callMacro("setHwPWMforTilt", [currentLevelTilt/LEVEL_MAX, commandID++]);
    }
  });

  $("#downBtn").click(function(){
    if(currentLevelTilt > LEVEL_MIN){
      currentLevelTilt -= 1;
      webiopi().callMacro("setHwPWMforTilt", [currentLevelTilt/LEVEL_MAX, commandID++]);
    }
  });

  $("#leftBtn").click(function(){
    if(currentLevelPan > LEVEL_MIN){
      currentLevelPan -= 1;
      webiopi().callMacro("setHwPWMforPan", [currentLevelPan/LEVEL_MAX, commandID++]);
    }
  });

  $("#rightBtn").click(function(){
    if(currentLevelPan < LEVEL_MAX){
      currentLevelPan += 1;
      webiopi().callMacro("setHwPWMforPan", [currentLevelPan/LEVEL_MAX, commandID++]);
    }
  });

  $("#shotBtn").click(function(){
    $.get("http://(RaspberryPiのアドレス):8080/0/action/snapshot", function(data){
      alert("Shot Success!!");
    });
  });

});

以前はカメラ操作をカッコ良く(?)スライダUIにしていたのですが、スマホで片手でカメラの縦横操作をするならボタン操作の方がラクかなーと思いましたので、ボタンUIに変更しています。

ボタンUIへの変更に伴い、WebIOPiのPython定義の関数呼び出し(callMacro)の引数の渡し方などを変更していたりしていますが、ポイントはカメラの撮影機能のところ($(“#shotBtn”).click()内)です。motionはストリーム用httpサーバ以外に制御用httpサーバも起動するようになっていて、そこで公開されているURIを叩けば撮影ができるようになっています。撮影された写真は、先に説明したとおりimgディレクトリに保存されます。

本当は撮影した写真をすぐに閲覧できるようなギャラリーUIまで作れればベストなのですが、それはハードルがグッと上がりそうなので、いつか余裕があれば。

最後、”python/servo-control.py”です。こちらは、以前作成したものから変えていませんが、再掲しておきます。

mport webiopi
import time
import wiringpi2 as wiringpi

SERVO_PAN  = 12
SERVO_TILT = 13

# SERVO_PAN  (Left)90 ... 0 ... -90(Right)
# SERVO_TILT (Down)90 ... 0 ... -90(UP)
SERVO_PAN_TRIM  = 12 # degree
SERVO_TILT_TRIM =  0 # degree

SERVO_PAN_LEFT_LIMIT  =  60 # degree
SERVO_PAN_RIGHT_LIMIT = -60 # degree
SERVO_TILT_DOWN_LIMIT =  40 # degree
SERVO_TILT_UP_LIMIT   = -40 # degree

##### SERVO SPECIFICATION #####
SERVO_ANGLE_MIN = -90 # degree
SERVO_ANGLE_MAX =  90 # degree
SERVO_PULSE_MIN = 0.5 # ms
SERVO_PULSE_MAX = 2.4 # ms
SERVO_CYCLE     =  20 # ms
###############################

#### WIRINGPI SPECIFICATION ####
PWM_WRITE_MIN = 0
PWM_WRITE_MAX = 1024
################################

SERVO_DUTY_MIN = SERVO_PULSE_MIN/SERVO_CYCLE
SERVO_DUTY_MAX = SERVO_PULSE_MAX/SERVO_CYCLE

SERVO_PAN_DUTY_MIN  =  (SERVO_DUTY_MAX - SERVO_DUTY_MIN) / (SERVO_ANGLE_MAX - SERVO_ANGLE_MIN) * ((SERVO_PAN_LEFT_LIMIT +SERVO_PAN_TRIM)  - SERVO_ANGLE_MIN) + SERVO_DUTY_MIN
SERVO_PAN_DUTY_MAX  =  (SERVO_DUTY_MAX - SERVO_DUTY_MIN) / (SERVO_ANGLE_MAX - SERVO_ANGLE_MIN) * ((SERVO_PAN_RIGHT_LIMIT+SERVO_PAN_TRIM)  - SERVO_ANGLE_MIN) + SERVO_DUTY_MIN
SERVO_TILT_DUTY_MIN =  (SERVO_DUTY_MAX - SERVO_DUTY_MIN) / (SERVO_ANGLE_MAX - SERVO_ANGLE_MIN) * ((SERVO_TILT_DOWN_LIMIT+SERVO_TILT_TRIM) - SERVO_ANGLE_MIN) + SERVO_DUTY_MIN
SERVO_TILT_DUTY_MAX =  (SERVO_DUTY_MAX - SERVO_DUTY_MIN) / (SERVO_ANGLE_MAX - SERVO_ANGLE_MIN) * ((SERVO_TILT_UP_LIMIT  +SERVO_TILT_TRIM) - SERVO_ANGLE_MIN) + SERVO_DUTY_MIN

SERVO_PAN_PWM_WRITE_MIN  = PWM_WRITE_MAX * SERVO_PAN_DUTY_MIN
SERVO_PAN_PWM_WRITE_MAX  = PWM_WRITE_MAX * SERVO_PAN_DUTY_MAX
SERVO_TILT_PWM_WRITE_MIN = PWM_WRITE_MAX * SERVO_TILT_DUTY_MIN
SERVO_TILT_PWM_WRITE_MAX = PWM_WRITE_MAX * SERVO_TILT_DUTY_MAX

def getServoPanPWMvalue(val):
  # This function returns 0 ... 1024
  pwm_value = int((SERVO_PAN_PWM_WRITE_MAX - SERVO_PAN_PWM_WRITE_MIN) * val + SERVO_PAN_PWM_WRITE_MIN)
  return pwm_value

def getServoTiltPWMvalue(val):
  # This function returns 0 ... 1024
  pwm_value = int((SERVO_TILT_PWM_WRITE_MAX - SERVO_TILT_PWM_WRITE_MIN) * val + SERVO_TILT_PWM_WRITE_MIN)
  return pwm_value

wiringpi.wiringPiSetupGpio()
wiringpi.pinMode(SERVO_PAN, wiringpi.GPIO.PWM_OUTPUT)
wiringpi.pinMode(SERVO_TILT, wiringpi.GPIO.PWM_OUTPUT)
wiringpi.pwmSetMode(wiringpi.GPIO.PWM_MODE_MS)
wiringpi.pwmSetClock(375) # 50Hz
wiringpi.pwmWrite(SERVO_PAN, getServoPanPWMvalue(0.5))
wiringpi.pwmWrite(SERVO_TILT, getServoTiltPWMvalue(0.5))

webiopi.setDebug()

def setup():
  webiopi.debug("Script with macros - Setup")

def loop():
  webiopi.sleep(5)

def destroy():
  webiopi.debug("Script with macros - Destroy")

@webiopi.macro
def setHwPWMforPan(duty, commandID):
  wiringpi.pwmWrite(SERVO_PAN, getServoPanPWMvalue(float(duty)))

@webiopi.macro
def setHwPWMforTilt(duty, commandID):
  wiringpi.pwmWrite(SERVO_TILT, getServoTiltPWMvalue(float(duty)))

これで出来上がりです。UIはこんな感じになります。

Screenshot_2015-12-05-12-50-18

これはこれでシンプルで良いのですが、もう少し情報量があっても良いなあと思うので、次はこれに温度とか照度とかのセンシング結果を表示できるようにしていきたいと思います。