気の向くままに辿るIT/ICT/IoT
IoT・電子工作

Raspberry Pi/pywebview/RPI.GPIO/28BYJ-48でパンチルト

ホーム前へ次へ
Raspberry Piって?

Raspberry Pi/pywebview/RPI.GPIO/28BYJ-48でパンチルト

Raspberry Pi/pywebview/RPI.GPIO/28BYJ-48でパンチルト

2022/05/04

 Raspberry Piとステッピングモータ28BYJ-48 5V/モータドライバULN2003でパンチルトのうち、パン(左右首振り)機能を作ろうと思い見つけたWebIOPiを使ったプログラムpywebviewに移植してみた話。

 流用すべく、解析してみるとPythonとJavaScript間でマクロを使って相互に通信している部分は...、似たようなことができるものもあるのか?と検索してみるといわゆるデスクトップアプリを作ることができるというEelやpywebviewの存在を知った次第。

 この組み合わせだと3B+には厳しいのか、映像を表示させるとパン機能が効かなくなるので動画でも映像なしでカメラのみ動かしています。

 また、完成してみれば、薄々感じていたとおり、デスクトップ版であるこれらでは、リモート操作できない模様であるため、他をあたることにし、結果、Raspberry Pi/Flask/WebSocket/RPI.GPIO/28BYJ-48にしましたが、せっかくなので公開しておこうかと。

pywebviewのインストール

USER@raspberrypi:~ $ pip install pywebview
USER@raspberrypi:~ $

 pywebviewは、pip(pip3)でインストールできます。

RPi.GPIOのインストール

USER@raspberrypi:~ $ pip install RPi.GPIO
USER@raspberrypi:~ $

 RPi.GPIOは、標準で入っている場合もあると思いますが、なければ、pip(pip3)でインストールできます。

Pythonスクリプト

USER@raspberrypi:~ $ cat script.py
import threading
import time
import sys
from flask import Flask, render_template
import webview
import RPi.GPIO as GPIO
 
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
 
class Stepper:
 
  GPIO= None
 
  def __init__(self, Id=0,IO=[17, 27, 22, 10], PulseDelay = 0.03):
    #set variable value
    self.StepperId=Id
    self.StepperIO= IO
    self.PulseDelay = PulseDelay
    self.CurrentPosition = 0
    self.TargetPosition = 0
    self.ThreadRunning = False
    self.StopThread= True
    self.StepperTable= [[1,0,0,1] , [1,1,0,0] , [0,1,1,0] , [0,0,1,1]]
 
  def init(self):
    response = {
      'Hello from Python {0}'.format(sys.version)
    }
    '''
    response = {
      'message': 'Hello from Python {0}'.format(sys.version)
    }
    '''
    return response
 
 
  def begin(self):
    #set GPIO OUTPUT
    for i in self.StepperIO:
      GPIO.setup(i,GPIO.OUT)
      GPIO.output(i,0)
    #start thread
    self.StopThread=False
    t = threading.Thread(target=self.stepperThread)
    t.start()
 
  def setCoil(self):
      StepperIdx = self.CurrentPosition & 3
      print("StepperIdx : " + str(StepperIdx))
      for i in range(4):
        GPIO.output(self.StepperIO[i], self.StepperTable[i][StepperIdx])
        print("self.StepperIO[i]:self.StepperTable[i][StepperIdx] / " + str(self.StepperIO[i]) + " : " + str(self.StepperTable[i][StepperIdx]))
 
  def stepperThread(self):
    self.ThreadRunning=True
    while not self.StopThread:
      if self.CurrentPosition != self.TargetPosition:
        if self.CurrentPosition > self.TargetPosition:
          self.CurrentPosition -= 1
        else:
          self.CurrentPosition += 1
        print("self.CurrentPosition" + str(self.CurrentPosition))
        self.setCoil()
        time.sleep(self.PulseDelay)
      else:
        time.sleep(0.01)
    self.ThreadRunning=False
    self.StopThread=True
 
  def moveTo(self, Target):
    self.TargetPosition = Target
 
  def move(self,StepCount):
    Target= self.TargetPosition + StepCount
    self.moveTo(Target)
 
  def stop(self):
    TargetPosition= CurrentPosition
 
stepper0 = Stepper(Id=0,IO=[17, 27, 22, 10])
stepper1 = Stepper(Id=1,IO=[23, 24, 25, 4])
 
stepper0.begin()
 
#stepper1 is not activate right now
#remove the rem on the next line to activate it
#stepper1.begin()
 
def StepperMove(_StepperId,_Step):
  StepperId= int(_StepperId)
  Step = int(_Step)
  if StepperId == 0 :
    stepper0.move(Step)
  else:
    stepper1.move(Step)
 
def StepperMoveTo(_StepperId,_Step):
  StepperId= int(_StepperId)
  Step = int(_Step)
  if StepperId == 0 :
    stepper0.moveTo(Step)
  else:
    stepper1.moveTo(Step)
 
def StepperStop(_StepperId):
  StepperId= int(_StepperId)
  if StepperId == 0:
    stepper0.stop()
  else:
    stepper1.stop()
 
def GetStepperPosition(_StepperId):
  StepperId= int(_StepperId)
  if StepperId == 0:
    response = stepper0.CurrentPosition
  else:
    response = stepper1.CurrentPosition
  return response
 
def GetStepperTarget(_StepperId):
  StepperId= int(_StepperId)
  if StepperId == 0:
    response = str(stepper0.TargetPosition)
  else:
    response = str(stepper1.TargetPosition)
  return response
 
def expose(window):
  window.evaluate_js('pywebview.api.StepperMove()')  
  window.evaluate_js('pywebview.api.StepperMoveTo()')  
  window.evaluate_js('pywebview.api.StepperStop()')  
  window.evaluate_js('pywebview.api.GetStepperPosition()')  
  window.evaluate_js('pywebview.api.GetStepperTarget()')  
 
if __name__ == "__main__":
 
  api = Stepper()
 
  window = webview.create_window('Camera Angle Adjust', url="templates/index.html", js_api=api)
  window.expose(StepperMove, StepperMoveTo, StepperStop, GetStepperPosition, GetStepperTarget)
  webview.start(expose, window, debug=True)
 
  GPIO.cleanup()
USER@raspberrypi:~ $

 script.pyは、こんな感じ。

 JavaScriptとPythonのやりとり部分は、Javascript APIExposeとをにらめっこしつつ、トライ&エラーの末、完成。

JavaScriptソース

USER@raspberrypi:~ $ cat static/camera.js
window.addEventListener('pywebviewready', function() {
 var title = document.getElementById('cam_title')
 title.innerHTML = 'カメラ調整'
 setInterval(ReadPosition,500);
 //initialize();
 ReadTarget(0);
 //ReadTarget(1);
})
 
function initialize() {
  
}
 
function ReadPosition()
{
 var pos = "0";
 pywebview.api.GetStepperPosition(pos);
}
 
function SetTarget(StepperId,value)
{
 var nStep = parseInt(value);
 pywebview.api.StepperMoveTo(StepperId , nStep);
}
 
function ReadTarget(StepperId)
{
 pywebview.api.GetStepperTarget(StepperId);
}
 
function Move(StepperId , StepCount) {
 pywebview.api.StepperMove(StepperId ,StepCount)
 
 var TargetP = "TargetP" + StepperId.toString();
 var CurrentP = "CurrentP" + StepperId.toString();
 var newTarget = parseInt(document.getElementById(TargetP).value);
 newTarget = newTarget + parseInt(StepCount);
 document.getElementById(TargetP).value = newTarget.toString();
 var cp = document.getElementById(CurrentP);
 
 if (Math.sign(StepCount) == 1) {
  for (i=0;i<StepCount;i++) {
   cp.value = parseInt(cp.value)+1;
  }
 } else {
  for (i=StepCount;i<0;i++) {
   cp.value = parseInt(cp.value)-1;
  }
 }
}
 
function Forward(StepperId)
{
 var nStep = "nStep" + StepperId.toString();
 var StepValue = parseInt(document.getElementById(nStep).value);
 Move(StepperId,StepValue);
}
 
function Backward(StepperId)
{
 var nStep = "nStep"+StepperId.toString();
 var StepValue = parseInt(document.getElementById(nStep).value)
 Move(StepperId,-StepValue);
}
USER@raspberrypi:~ $

 static/camera.jsは、こんな感じ。

 WebIOPiからpywebviewへの移植に伴い、それなりに編集しました。

 オリジナルソースだとid=Current0のinput type=textタグの内容は、美しくもボタン押下ごとにカウントダウン/カウントアップします。

 が、そこ端折った、このロジックだとid=Current0のinput type=textタグは、id=Tartget0のそれと同じなので不要ですが残しました。

 一部の関数や#ws-status行など使っていない部分、オリジナルにはないHTMLファイルに残したid=return0のinput type=textタグ用DOMなどコメントアウトしつつ、場合によっては使うかなという部分も。

CSSソース

USER@raspberrypi:~ $ cat static/camera.css
#response-container {
  display: none;
  padding: 3rem;
  margin: 3rem 5rem;
  font-size: 120%;
  border: 5px dashed #ccc;
}
.myButton {
  -moz-box-shadow:inset 0px 1px 0px 0px #ffffff;
  -webkit-box-shadow:inset 0px 1px 0px 0px #ffffff;
  box-shadow:inset 0px 1px 0px 0px #ffffff;
  background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #ffffff), color-stop(1, #f6f6f6));
  background:-moz-linear-gradient(top, #ffffff 5%, #f6f6f6 100%);
  background:-webkit-linear-gradient(top, #ffffff 5%, #f6f6f6 100%);
  background:-o-linear-gradient(top, #ffffff 5%, #f6f6f6 100%);
  background:-ms-linear-gradient(top, #ffffff 5%, #f6f6f6 100%);
  background:linear-gradient(to bottom, #ffffff 5%, #f6f6f6 100%);
  background-color:#ffffff;
  -moz-border-radius:6px;
  -webkit-border-radius:6px;
  border-radius:6px;
  border:1px solid #dcdcdc;
  display:inline-block;
  cursor:pointer;
  color:#666666;
  font-family:Arial;
  font-size:15px;
  font-weight:bold;
  padding:6px 24px;
  text-decoration:none;
  text-shadow:0px 1px 0px #ffffff;
  width: 130px;
  height:30px;
  margin:1px;
}
.myButton:hover {
  background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #f6f6f6), color-stop(1, #ffffff));
  background:-moz-linear-gradient(top, #f6f6f6 5%, #ffffff 100%);
  background:-webkit-linear-gradient(top, #f6f6f6 5%, #ffffff 100%);
  background:-o-linear-gradient(top, #f6f6f6 5%, #ffffff 100%);
  background:-ms-linear-gradient(top, #f6f6f6 5%, #ffffff 100%);
  background:linear-gradient(to bottom, #f6f6f6 5%, #ffffff 100%);
  background-color:#f6f6f6;
  padding:6px 24px;
}
.myButton:active {
  color:#ff0000;
  position:relative;
  top:1px;
  padding:6px 24px;
}
USER@raspberrypi:~ $

 static/camera.cssは、こんな感じ。

 CSSに関しては、index.html内にあった元のソースを外部ファイルとして分離しただけです。

HTMLソース

USER@raspberrypi:~ $ cat templates/index.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Stepper</title>
 
<link rel="stylesheet" href="../static/camera.css">
 
<style>
#refblock
{
display:none
/*
display:block
*/
}
</style>
</head>
<body>
<center>
<h1 id="cam_title" style="font-size:140% ;font-weight:normal ;"> My Stepper</h1>
<div style="margin-left:auto;margin-right:auto ;">
<div><iframe src="http://192.168.0.22:8080/stream" width="50%" height="250px"></iframe></div>
<div><button class="myButton" type="button" onclick="Backward(0)">右</button>
<button class="myButton" type="button" onclick="Forward(0)">左</button></div>
</div>
<div id="refblock">
<div><input type="text" id="CurrentP0" name="CurrentP0" value="0" readonly></div>
<div><input type="text" id="TargetP0" name="TargetP0" value="0" onchange="SetTarget(0,this.value)"></div>
<div><input type="text" id="nStep0" name="nStep0" value="100"></div>
</div>
</center>
<script type="text/javascript" src="../static/camera.js"></script>
</body>
</html>
USER@raspberrypi:~ $

 pywebviewの場合、FirefoxとかChrome/Chromiumなどではなく、webviewに表示されるわけですが、templates/index.htmlは、こんな感じ。

ほか、中身は少し編集しました。

操作例

 ソース通りなら、ラズパイのGPIO(BCM)ピン17/18/27/22を、それぞれULN2003のIN1/IN2/IN3/IN4に接続し、電源投入、端末でustreamer --host=0.0.0.0 &、script.pyを実行(python script.py)、表示されるwebview画面でボタン操作。

 左/右ボタンをクリックすると、Number of Step分だけ、Target Positionが増減、元プログラムでは、Current Positonは、ほぼリアルタイムに値が増減しますが、今回、端折ってTarget Positionと同じ値を示します。

 script.pyに追記したprint文を有効にしてあるので端末には、ボタン押下ごとにGPIOピンの状態も表示されます。

 視覚的に不要なinputタグのtext類を非表示にした上、例えば、ustreamerやmjpg-streamerなどの映像を加えるだけで動作中のカメラのパンチルト状態をリアルタイムで見て取れるようになるのですが、ラズパイ3B+には荷が重いのか、映像表示させるとパンが機能せず、今回の要件にはマッチしませんでした。

 というか、それ以前にpywebview(やeelが実現するのは)デスクトップアプリであり、外部サイトを表示することはできてもリモートホストのブラウザから容易に遠隔操作できないので見送ることになったわけですが。

ホーム前へ次へ