以前にもブラウザで操作できるRaspberryPiの2足歩行ロボットを作成しましたが、「webiopi」というWEBサーバーを使ったものでした。↓
これはこれで良いのですが、webiopiはラズパイ専用に用意されたモジュールであり、他に使える場面が無く導入も少し面倒だったり(極端に言えば無駄な知識になり得る)、更新も結構前で止まっているので、いつ使えなくか分かりません。
そういった事を加味し、今回はオープンソースのWEBサーバーで有名な「Apache」を導入し、CGIを使ってブラウザからpythonでサーボモーターを制御するRaspberryPi2足歩行ロボを作成いたしました。
当初は、pythonの簡易WEBサーバーであるhttp serverでの作成を考えておりましたが、jsからcgi(python)へデータをpostする際のタイムラグが無視できないレベルだったので、Apacheを使う事にしました。※2022/5/28追記:関係ないかもしれません。。
webiopiからの変更点はApacheに変更しただけでなく、以下の改善点も追加いたしました。
・操作ボタンを押している間だけ歩行し、離すと止まる
今回はこのロボットの作成方法についてまとめようと思います。//
目次
初めに
全体的な構成
おおまかな構成自体はwebiopi使用時とほとんど変わりはないので、過去記事のリンクを張っておきます。↓
プログラムからサーボモータを制御する一連の流れとしましては、
ブラウザ(html)→javascript→CGI(python)→PCA9685→サーボモーター
となります。
Apacheの導入と各種設定
インストール
それではまず、Apacheをインストールします。
1 2 3 4 |
$ sudo apt-get update $ sudo apt-get upgrade $ sudo apt-get install apache2 $ sudo apt-get install apache2-dev |
Apacheがインストールされれば自動的にWEBサーバーが立ち上がるので、ラズパイからはローカルホスト(127.0.0.1)、他のデバイスからはラズパイのIPをブラウザで指定してやれば、デフォルトのApacheのWEBページが表示されます。
初期のルートディレクトリは、
/var/www/html/
になっていますので、その配下のindex.htmlを差し替えてやれば、WEBページを変更できます。
ちなみに、Apacheインストール後は、ラズパイの電源が投入された段階でWEBサーバーが立ち上がる仕様になっていますので、変更したい場合は別途設定の変更が必要です。また、ラズパイのIPは固定した方が便利ですが、その辺についての説明は今回割愛します。
CGIの設定
CGIを使ってpythonスクリプトを動作させるための設定を行います。
まず、CGIのシンボリックリンク(ショートカットのようなもの)を貼ります。
1 |
sudo ln -s /etc/apache2/mods-available/cgi.load /etc/apache2/mods-enabled/cgi.load |
続いて、pythonでCGIが実効出来るように、.pyを追加します。
1 2 3 4 |
$ sudo vi /etc/apache2/mods-available/mime.conf # AddHandler cgi-script .cgi ↓ コメントを外し、「.py」を追記 AddHandler cgi-script .cgi .py |
続いて、CGIのディレクトリ場所を指定します。
1 2 3 4 5 6 7 8 |
$ sudo vi /etc/apache2/conf-available/serve-cgi-bin.conf [変更前] ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/ <Directory "/usr/lib/cgi-bin"> [変更後] ScriptAlias /cgi-bin/ /var/www/html/cgi-bin/ <Directory "/var/www/html/cgi-bin"> |
今回、ルートディレクトリ配下にファイル一式を格納しますので、上記のようにしています。変更前が/var/www/html/cgi-bin/なら、変更は不要です。
もちろん、他の任意の場所に指定する事も可能です。(その場合はルートディレクトリも変更する事をおすすめします。)
Apacheからi2cを制御するための設定
ハマりどころでした。。これをしなくとも、とりあえずCGIは動くのですが、例えばpythonスクリプトにPCA9685の専用ライブラリをインポートしていると「エラー500」のサーバーエラーを吐きます。つまり、i2cデバイスを制御するならば、以下のようなi2cにアクセスできる設定が必要になります。
1 2 |
$ sudo gpasswd -a www-data i2c $ sudo gpasswd -a www-data gpio |
これでApacheの導入と設定はひと段落なので、設定反映のためにApacheを再起動しておきます。
1 |
sudo service apache2 restart |
次は、ルートディレクトリに必要なファイルを作成していきます。
html,css,javascript,python
図のように、ルートディレクトリの/var/www/html/配下に必要なファイルを作成していきます。
html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>テスト</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="style.css"> </head> <body> <div class="cam"> <img src="http://ローカルIP:8080/?action=stream"/> </div> <main> <ul> <div class="a"> <li id="forward" class="ledoff">前進</li> </div> <div class="bc"> <div class="b"> <li id="left" class="ledoff">左旋回</li> </div> <div class="n"> <li></li> </div> <div class="c"> <li id="right" class="ledoff">右旋回</li> </div> </div> <div class="d"> <li id="backward" class="ledoff">後退</li> </div> </ul> </main> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script> <script src="main.js"></script> </body> </html> |
操作画面になります。jsからpythonへデータをpostする為に非同期通信が必要なので、Ajaxは必須です。
css
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
* { margin: 0px; padding: 0px; } body { max-width: 600px; font-size: 25px; width: 100%; -webkit-touch-callout: none; -webkit-user-select: none; } img { width: 100%; } main { height: 40vh; background: skyblue; } ul { display: block; height: 40vh; list-style: none; padding-top: 10px; } .bc { display: flex; } .a ,.bc, .d { height: 12vh; } li { width: 100px ; height: 90% ; margin-left: auto; margin-right: auto; background: yellow; } .b { margin-left: auto; } .c { margin-right: auto; } ul li { text-align: center; } .a li, .b li, .c li, .d li { border: solid 1px; } .ledon { background: #f88888; } .n li { background: skyblue; } a:active { color: #ff2020; } |
スタイリングしているだけですが、1つ重要な部分があります。body{}に記述しているwebkitです。
ここもハマりどころでしたが、というのも、今回後述するjsで「スマホでボタンを押している間イベント発火」としているのですが、そうするとボタンのtextが「コピーする」などのテキスト修正モードに入ってしまうのです。
ベタな解決方法として「user-select: none;」がありますが、これが効かない。。
調べた結果、どうもアンドロイド端末だと「webkit…」が必要になるみたいです。
javascript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
$(function(){ let motor = "STOP"; // 「前進」ボタンが押されたときのイベント処理 $('#forward').bind('touchstart', function() { // 押されたとき if(motor == 'STOP') { $(this).addClass('ledon'); change_motor('FOWARD'); ff1 = setInterval(function(){ change_motor('FOWARD'); }, 3000); } }).bind('touchend', function() { // 離したとき $(this).removeClass('ledon'); //change_motor('STOP'); clearInterval(ff1); }); // 「後退」ボタンが押されたときのイベント処理 $('#backward').bind('touchstart', function() { if(motor == "STOP") { $(this).addClass('ledon'); change_motor('BACKWARD'); ff1 = setInterval(function(){ change_motor('BACKWARD'); }, 3000); } }).bind('touchend', function() { $(this).removeClass('ledon'); //motor = "STOP"; //change_motor('STOP'); clearInterval(ff1); }); // 「右」ボタンが押されたときのイベント処理 $('#right').bind('touchstart', function() { if(motor == "STOP") { $(this).addClass('ledon'); change_motor('RIGHT'); ff1 = setInterval(function(){ change_motor('RIGHT'); }, 1400); } }).bind('touchend', function() { $(this).removeClass('ledon'); //motor = "STOP"; //change_motor('STOP'); clearInterval(ff1); }); // 「左」ボタンが押されたときのイベント処理 $('#left').bind('touchstart', function() { if(motor == "STOP") { $(this).addClass('ledon'); change_motor('LEFT'); ff1 = setInterval(function(){ change_motor('LEFT'); }, 1300); } }).bind('touchend', function() { $(this).removeClass('ledon'); //motor = "STOP"; //change_motor('STOP'); clearInterval(ff1); }); function change_motor2() { motor = 'STOP'; } // 関数:モーターを動かすマクロ呼び出し function change_motor(typee) { motor = typee; if(typee == "FOWARD") { // 前進 //w().callMacro('FW'); console.log(typee); $.ajax({ url: 'cgi-bin/recieve.py', type: 'post', data: {name: 'hhhhh' } }).done(function(data){ console.log(data); change_motor2(); }).fail(function(){ console.log('failed'); }); } else if(typee == "BACKWARD") { //w().callMacro('BK'); console.log(typee); $.ajax({ url: 'cgi-bin/recieve.py', type: 'post', data: {name: 'iiii' } }).done(function(data){ console.log(data); change_motor2(); }).fail(function(){ console.log('failed'); }); } else if(typee == "RIGHT") { //w().callMacro('RT'); console.log(typee); $.ajax({ url: 'cgi-bin/recieve.py', type: 'post', data: {name: 'jjjj' } }).done(function(data){ console.log(data); change_motor2(); }).fail(function(){ console.log('failed'); }); } else if(typee == "LEFT") { //w().callMacro('LT'); console.log(typee); $.ajax({ url: 'cgi-bin/recieve.py', type: 'post', data: {name: 'kkkk' } }).done(function(data){ console.log(data); change_motor2(); }).fail(function(){ console.log('failed'); }); } else if(typee == "STOP") { //w().callMacro('ST'); console.log(typee); $.ajax({ url: 'cgi-bin/recieve.py', type: 'post', data: {name: 'ssss' } }); } } }); |
少し長ったらしいですが、このjsに関してはほぼ同じ内容の事を過去記事でまとめていますので、そちらを参照ください。↓
追加になっている部分だけ説明しておきます。
ボタンを連続でタップした際に、同じ動作のイベント発火情報がCGIにpostされていたので、その際にサーボモーターが狂った動作を起こしていました。
これを回避する為に、jsからイベント発火のデータがpythonに送られた場合、一連のサーボモーターの動作が終了するまでは追加のイベント発火は受け付けないようにしました。
具体的には、イベントはmotor変数が「STOP」の場合(1度ボタンを上げた状態)でボタンを押すと発火するようになっているので、ボタンを上げても、CGIのpythonからの応答がない限りSTOPにはならないようにしました。
done(function(data) { でCGIからサーボモータのー動作終了の合図が返ってくるので、その時にchange_motor2関数を呼び出し、motor変数にSTOPを代入しています。
python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 |
#!/usr/bin/python3 # -*- coding: utf-8 -*- import cgi import time import Adafruit_PCA9685 import cgitb cgitb.enable() pwm = Adafruit_PCA9685.PCA9685() pwm.set_pwm_freq(60) p = 400 i = 395 n = 375 t = 362 form = cgi.FieldStorage() recieve = form.getvalue('name') def ff(): global p global i global n global t while(n <= 500): pwm.set_pwm(3, 0, t) pwm.set_pwm(1, 0, n) time.sleep(0.0005) #time.sleep(1) n = n + 1 if t <= 450: t = t + 1 while(i <= 495): pwm.set_pwm(2, 0, i) time.sleep(0.0005) #time.sleep(1) i = i + 1 while(n >= 375): pwm.set_pwm(3, 0, t) pwm.set_pwm(1, 0, n) time.sleep(0.0005) #time.sleep(1) n = n - 1 if t >= 362: t = t - 1 while(i >= 395): pwm.set_pwm(2, 0, i) time.sleep(0.0005) #time.sleep(1) i = i - 1 while(t >= 250): pwm.set_pwm(3, 0, t) pwm.set_pwm(1, 0, n) time.sleep(0.0005) #time.sleep(1) t = t - 1 if n >= 300: n = n - 1 while(p >= 300): pwm.set_pwm(0, 0, p) time.sleep(0.0005) #time.sleep(1) p = p - 1 while(t <= 362): pwm.set_pwm(3, 0, t) pwm.set_pwm(1, 0, n) time.sleep(0.0005) #time.sleep(1) t = t + 1 if n <= 375: n = n + 1 while(p <= 400): pwm.set_pwm(0, 0, p) time.sleep(0.0005) #time.sleep(1) p = p + 1 print('Content-type: text/html\n') print(recieve) if recieve == 'hhhhh': ff() #while(n <= 500): #pwm.set_pwm(3, 0, t) #pwm.set_pwm(1, 0, n) #time.sleep(0.0005) #time.sleep(1) #n = n + 1 #if t <= 450: #t = t + 1 #while(i <= 495): #pwm.set_pwm(2, 0, i) #time.sleep(0.0005) #time.sleep(1) #i = i + 1 #while(n >= 375): #pwm.set_pwm(3, 0, t) #pwm.set_pwm(1, 0, n) #time.sleep(0.0005) #time.sleep(1) #n = n - 1 #if t >= 362: #t = t - 1 #while(i >= 395): #pwm.set_pwm(2, 0, i) #time.sleep(0.0005) #time.sleep(1) #i = i - 1 #while(t >= 250): #pwm.set_pwm(3, 0, t) #pwm.set_pwm(1, 0, n) #time.sleep(0.0005) #time.sleep(1) #t = t - 1 #if n >= 300: #n = n - 1 #while(p >= 300): #pwm.set_pwm(0, 0, p) #time.sleep(0.0005) #time.sleep(1) #p = p - 1 #while(t <= 362): #pwm.set_pwm(3, 0, t) #pwm.set_pwm(1, 0, n) #time.sleep(0.0005) #time.sleep(1) #t = t + 1 #if n <= 375: #n = n + 1 #while(p <= 400): #pwm.set_pwm(0, 0, p) #time.sleep(0.0005) #time.sleep(1) #p = p + 1 elif recieve == 'iiii': while(n <= 500): pwm.set_pwm(3, 0, t) pwm.set_pwm(1, 0, n) time.sleep(0.0005) n = n + 1 if t <= 450: t = t + 1 while(i >= 295): pwm.set_pwm(2, 0, i) time.sleep(0.0005) i = i - 1 while(n >= 375): pwm.set_pwm(3, 0, t) pwm.set_pwm(1, 0, n) time.sleep(0.0005) n = n - 1 if t >= 363: t = t - 1 while(i <= 395): pwm.set_pwm(2, 0, i) time.sleep(0.0005) i = i + 1 while(t >= 250): pwm.set_pwm(3, 0, t) pwm.set_pwm(1, 0, n) time.sleep(0.0005) t = t - 1 if n >= 300: n = n - 1 while(p <= 500): pwm.set_pwm(0, 0, p) time.sleep(0.0005) p = p + 1 while(t <= 363): pwm.set_pwm(3, 0, t) pwm.set_pwm(1, 0, n) time.sleep(0.0005) t = t + 1 if n <= 375: n = n + 1 while(p >= 400): pwm.set_pwm(0, 0, p) time.sleep(0.0005) p = p - 1 print('Content-type: text/html\n') print(recieve) elif recieve == 'jjjj': while(n <= 500): pwm.set_pwm(3, 0, t) pwm.set_pwm(1, 0, n) time.sleep(0.0005) n = n + 1 if t <= 450: t = t + 1 while(i >= 295): pwm.set_pwm(2, 0, i) time.sleep(0.0005) i = i - 1 while(n >= 375): pwm.set_pwm(3, 0, t) pwm.set_pwm(1, 0, n) time.sleep(0.0005) n = n - 1 if t >= 363: t = t - 1 while(i <= 395): pwm.set_pwm(2, 0, i) time.sleep(0.0005) i = i + 1 print('Content-type: text/html\n') print(recieve) elif recieve == 'kkkk': while(t >= 250): pwm.set_pwm(3, 0, t) pwm.set_pwm(1, 0, n) time.sleep(0.0005) t = t - 1 if n >= 300: n = n - 1 while(p <= 500): pwm.set_pwm(0, 0, p) time.sleep(0.0005) p = p + 1 while(t <= 363): pwm.set_pwm(3, 0, t) pwm.set_pwm(1, 0, n) time.sleep(0.0005) t = t + 1 if n <= 375: n = n + 1 while(p >= 400): pwm.set_pwm(0, 0, p) time.sleep(0.0005) p = p - 1 print('Content-type: text/html\n') print(recieve) elif recieve == 'ssss': pass |
先頭にシェバンを記述しています。CGIなのでjsからこのファイルのURL宛てにpostされたデータがあれば、都度このファイルが実効されます。
postされてきたデータにあわせて歩く動作を決めています。各動作の終了時に、print()でjsに値を返しています。つまりこれがサーボモーターの動作が終了した合図になります。
完成
完成すると、こんな感じで動いてくれます。↓