树莓派智能车AlphaBot教程9:python-bottle

来自丢石头百科


上两章我们介绍了用webiopi实现网页控制,对网页控制也有了一定的了解。这一章我们介绍通过Python bottle实现网页控制。Bottle是一个非常小巧的微型Python web 框架。

Bottle

是一个非常小巧但高效的微型 

Python

Web 框架,它被设计为仅仅只有一个文件的Python模块,并且除Python标准库外,它不依赖于任何第三方模块。* 路由(Routing):将请求映射到函数,可以创建十分优雅的 URL* 模板(Templates):Pythonic 并且快速的 Python 内置模板引擎,同时还支持 mako, jinja2, cheetah 等第三方模板引擎* 工具集(Utilites):快速的读取 form 数据,上传文件,访问 cookies,headers 或者其它 HTTP 相关的 metadata* 服务器(Server):内置HTTP开发服务器,并且支持 paste, fapws3, bjoern, Google App Engine, Cherrypy 或者其它任何 WSGI HTTP 服务器


安装 Bottle

sudo apt-get install python-bottle

一个Hello World 程序

新建一个HelloWorld.py文件,并输入如下代码保存。

#!/usr/bin/python  
# -*- conding:utf-8 -*-  
   
from bottle import *           
                                              
@route('/helloworld/:yourwords')                    
def hello(yourwords):                                                            
    return 'hello world. ' + yourwords                                  
   
run(host='0.0.0.0', port=8080)

运行程序:

sudo python HelloWorld.py

在浏览器中输入:http://192.168.6.115:8080/helloworld/Bottle (IP地址改为树莓派实际地址)

就会显示如下页面。(改变helloworld后面的字符串,显示也不会不一样)


170553rea8qe78qvtz10vq.png

程序中用到两个Bottle组件,route()和run()函数。 route() 可以将一个函数与一个URL进行绑定,在上面的示例中,route 将 “/hello/:yourwords’ 这个URL地址绑定到了 hello(yourwords) 这个函数上. 我们获得请求后,hello() 函数返回简单的字符串. 最后,run() 函数启动服务器,并且我们设置它在 “localhost” 和 8080 端口上运行


通过Web控制RGB LED。 上面只是小试牛刀,下面再来一个酷炫的。通过网页控制RGB 彩灯,下面以RGB LED HAL模块的实力程序为例。 本程序一共包含四个文件color_picker.png color_range.png index.html main.py。其中前面两个文件为图片,index.html为HTML网页文件,main.py为脚本程序。

=== main.py: ===

#!/usr/bin/python
 
from bottle import get,request, route, run, static_file,template  
import time, threading
from neopixel import *
 
# LED strip configuration:
LED_COUNT      = 4      # Number of LED pixels.
LED_PIN        = 18      # GPIO pin connected to the pixels (must support PWM!).
LED_FREQ_HZ    = 800000  # LED signal frequency in hertz (usually 800khz)
LED_DMA        = 5       # DMA channel to use for generating signal (try 5)
LED_BRIGHTNESS = 255     # Set to 0 for darkest and 255 for brightest
LED_INVERT     = False   # True to invert the signal (when using NPN transistor level shift)
 
# Create NeoPixel object with appropriate configuration.
strip = Adafruit_NeoPixel(LED_COUNT, LED_PIN, LED_FREQ_HZ, LED_DMA, LED_INVERT, LED_BRIGHTNESS)
# Intialize the library (must be called once before other functions).
strip.begin()
strip.show()
 
rgb = 0
light_type = 'static'    #'static' 'breath' 'flash'
 
#Access the file root directory
@get("/")
def index():
    global rgb, light_type
    rgb = 0xffffff
    light_type = 'static'
    return static_file('index.html', './')
 
#Static files on the page need to be processed
@route('/<filename>')
def server_static(filename):
    return static_file(filename, root='./')
     
#get the rgb value by POST
@route('/rgb', method='POST')
def rgbLight():
    red = request.POST.get('red')
    green = request.POST.get('green')
    blue = request.POST.get('blue')
    #print('red='+ red +', green='+ green +', blue='+ blue)
    red = int(red)
    green = int(green)
    blue = int(blue)
    if 0 <= red <= 255 and 0 <= green <= 255 and 0 <= blue <= 255:
        global rgb
        rgb = (red<<8) | (green<<16) | blue
 
#get the type by POST
@route('/lightType', method='POST')
def lightType():
    global light_type
    light_type = request.POST.get('type')
    print("lightType="+light_type)
 
#Light cycle detection control
def lightLoop():
    global rgb, light_type
    flashTime = [0.3, 0.2, 0.1, 0.05, 0.05, 0.1, 0.2, 0.5, 0.2] #Blink time 
    flashTimeIndex = 0 #flash time index
    f = lambda x: (-1/10000.0)*x*x + (1/50.0)*x #Simulate the breathing light with parabola
    x = 0
    while True:
        if light_type == 'static':   
            for i in range(0,strip.numPixels()):
                strip.setPixelColor(i, rgb)     
                strip.show()
            time.sleep(0.05)
        elif light_type == 'breath': 
            red = int(((rgb &amp; 0x00ff00)>>8) * f(x))
            green = int(((rgb &amp; 0xff0000) >> 16) * f(x))
            blue = int((rgb &amp; 0x0000ff) * f(x))
            _rgb = int((red << 8) | (green << 16) | blue)
            for i in range(0,strip.numPixels()):
                strip.setPixelColor(i, _rgb)     
                strip.show()
            time.sleep(0.02)
            x += 1
            if x >= 200:
                x = 0
        elif light_type == 'flash': 
            for i in range(0,strip.numPixels()):
                strip.setPixelColor(i, rgb)     
                strip.show()
            time.sleep(flashTime[flashTimeIndex])
            for i in range(0,strip.numPixels()):
                strip.setPixelColor(i, 0)     
                strip.show()
            time.sleep(flashTime[flashTimeIndex])
            flashTimeIndex += 1
            if flashTimeIndex >= len(flashTime):
                flashTimeIndex = 0
 
 
#Open a new thread for rgb light display
t = threading.Thread(target = lightLoop)
t.setDaemon(True)
t.start()
 
#Set the server ip address and port (hint:you set your raspberry ip address before use )
run(host="0.0.0.0", port=8000)

这里采用的是W2812B灯珠,关于这个控制在这里就不在讲了。

@get("/"), 这作用是创建一个网页静态文件传输通道。流浪器访问时会打开目录下的index,html文件, @route('/<filename>') 这个是用来传输静态文件,两张图片的。 @route('/rgb', method='POST'),@route('/lightType', method='POST')是创建两个URL,分别用来传输RGB的值,和灯控制类型的。 获取到值是分别储存在reg,green,blue和light_type 中。 threading.Thread另外创建一个python线程,执行lightLoop()函数,实时处RGB LED的状态。RGB LED有静态显示,闪烁显示已经呼吸灯显示三种显示方式。

=== index.html代码: ===

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <!--Adapt to mobile phone size, not allowed to zoom-->
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>web rgb</title>
    <script src="http://code.jquery.com/jquery.js"></script>
    <style type="text/css">
        body,div,img{ border:0; margin:0; padding:0;}
    </style>
</head>
 
<body>
    <div style="width:100%; height:40px; line-height:40px; text-align:center; font-size:20px; color:white; background-color:blue; margin:auto">
        Controlling RGB LED with the web
    </div>
    <img width="300" height="300" src="color_range.png" id="myimg" style="display:none" alt="range"/>
    
    <div style="width:300px; height:300px; position:relative; text-align:center; margin:auto; margin-top:20px; margin-bottom:40px;" id="colorRange">
     
        <canvas id="mycanvas" width="300" height="300">
            Your browser does not support the html5 Canvas element
        </canvas>
         
        <img width="30" height="30" src="color_picker.png" id="picker" style="position:absolute; top:135px; left:135px;" alt="picker" /> 
    </div>
     
    <div style="font-size:20px;align:center;text-align:center;margin:auto; border:1px solid gray; border-radius:10px; width:320px; height:40px; line-height:40px;">
        <div>
            <input type="radio" name="radio1" value="static" checked/>static     
            <input type="radio" name="radio1" value="breath"/>breath     
            <input type="radio" name="radio1" value="flash"/>flash
        </div> 
    </div>   
</body>
    <script>
        var RadiusRange = 150;
        var RadiusPicker = 15;
        var offsetX = window.screen.width / 2 - RadiusRange;
        var offsetY = 60;
        var centerX = offsetX + RadiusRange;
        var centerY = offsetY + RadiusRange;
         
        var colorRange = $('#colorRange')[0];
        var colorPicker = $('#picker')[0];
        var myCanvas = $('#mycanvas')[0];
        var myImg = $('#myimg')[0];
        var ctx = myCanvas.getContext('2d');
        myImg.onload = function(){ctx.drawImage(myImg, 0, 0);}
         
        colorRange.addEventListener('touchstart', touch, false);  
        colorRange.addEventListener('touchmove', touch, false);       
        function touch(e)
        {
           var X = e.touches[0].clientX;
            var Y = e.touches[0].clientY;
            var x = X - centerX;
            var y = Y - centerY;
            if(Math.sqrt(x*x + y*y) < RadiusRange-5)
            {
                colorPicker.style.left = X - offsetX - RadiusPicker +'px';
                colorPicker.style.top = Y - offsetY - RadiusPicker +'px';
                 
                var rgba = ctx.getImageData(X-offsetX, Y-offsetY, 1, 1).data;
                var red = rgba['0'];
                var green = rgba['1'];
                var blue = rgba['2'];
                $.post('/rgb', {red: red, green: green, blue: blue});
            }  
             
            event.preventDefault(); 
        }   
         
        $('input').click(function() {
            var type = this.value;
            $.post('/lightType', {type: type});;
        });
    </script>
</html>

分析:

【1】 index.html文件包含<head>,<body>,<script>三部分 <head>部分中<script src="http://code.jquery.com/jquery.js"></script> 这个语句是通过网页链接引入jquery.js库文件,这个文件和上一张webiopi中的那个文件是一样的。


【2】 <body>部分是设置网页界面,包含一个网页标题,彩色图片,以及三个LED 显示类型选择按键。


【3】 <sritpt>为脚本,脚本中对touchstart事件和touchmove事件监听。这两个事件只在手机端起作用,所以在pc端访问时拖动鼠标,是不能选中颜色的。pc端相对应的事件为:onmousedown、onmousemove。 当事件触发时会调用touch()函数,获取当前的颜色并POST方式发送到/rgb。服务器端接受到传过来的数据就会触发main.py中的rgbLight()函数。 $('input').click(function() 这里是注册按键点击事件。但选择按键被按下时会触发函数,将当前的按键ID通过POST方式发送到/lightType 。从而会触发main.py中的lightType()函数。


170917y4vxnvmez0zn4v0v.jpeg

通过web控制AlphaBot2智能车 下载AlphaBot的程序,程序中web_Control目录即通过Bottle控制小车的。 工程目录下包含AlphaBot2.py,PCA9685.py,index.html,main.py四个文件。其中AlphaBot2.py为小车控制库文件,PCA9685.py这个是舵机控制库文件。主要看index.html和main.py这两个文件。

=== main.py代码: ===

#!/usr/bin/python
# -*- coding:utf-8 -*-
from bottle import get,post,run,request,template
from AlphaBot import AlphaBot
from PCA9685 import PCA9685
import threading
 
Ab = AlphaBot()
pwm = PCA9685(0x40)
pwm.setPWMFreq(50)
 
#Set the Horizontal servo parameters
HPulse = 1500  #Sets the initial Pulse
HStep = 0      #Sets the initial step length
pwm.setServoPulse(0,HPulse)
 
#Set the vertical servo parameters
VPulse = 1500  #Sets the initial Pulse
VStep = 0      #Sets the initial step length
pwm.setServoPulse(1,VPulse)
 
@get("/")
def index():
    return template("index")
     
@post("/cmd")
def cmd():
    global HStep,VStep
    code = request.body.read().decode()
    print(code)
    if code == "stop":
        HStep = 0
        VStep = 0
        Ab.stop()
    elif code == "forward":
        Ab.forward()
    elif code == "backward":
        Ab.backward()
    elif code == "turnleft":
        Ab.left()
    elif code == "turnright":
        Ab.right()
    elif code == "up":
        VStep = -5
    elif code == "down":
        VStep = 5
    elif code == "left":
        HStep = 5
    elif code == "right":
        HStep = -5
    return "OK"
     
def timerfunc():
    global HPulse,VPulse,HStep,VStep,pwm
     
    if(HStep != 0):
        HPulse += HStep
        if(HPulse >= 2500): 
            HPulse = 2500
        if(HPulse <= 500):
            HPulse = 500
        #set channel 2, the Horizontal servo
        pwm.setServoPulse(0,HPulse)    
         
    if(VStep != 0):
        VPulse += VStep
        if(VPulse >= 2500): 
            VPulse = 2500
        if(VPulse <= 500):
            VPulse = 500
        #set channel 3, the vertical servo
        pwm.setServoPulse(1,VPulse)   
     
    global t        #Notice: use global variable!
    t = threading.Timer(0.02, timerfunc)
    t.start()
     
t = threading.Timer(0.02, timerfunc)
t.setDaemon(True)
t.start()
 
run(host="0.0.0.0",port="8000")

程序分析;

@get("/") 创建一个创建一个网页文件传输通道,传输index.html文件。 @post("/cmd") 创建一下URL,对接受到的命令做出个中反应。其中forward,backward,turnleft,turnright,stop分别控制小车前进,后退,左转,右转,停止。up,down,left,right,控制舵机上下左右移动。 timerfunc()函数用来处理舵机转动的定时函数。 最后调用run() 函数启动服务器,并且我们设置它在 “localhost” 和 8080 端口上运行。

=== index.hmtl代码: ===

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AlphaBot</title>
    <link href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet" media="screen">
    <script src="http://code.jquery.com/jquery.js"></script>
    <script>
         
        $(function(){
            var isTouchDevice = "ontouchstart" in document.documentElement ? true : false;
            var BUTTON_DOWN   = isTouchDevice ? "touchstart" : "mousedown";
            var BUTTON_UP     = isTouchDevice ? "touchend"   : "mouseup";
             
            $("button").bind(BUTTON_DOWN,function(){
                $.post("/cmd",this.id,function(data,status){
                });
            });
 
            $("button").bind(BUTTON_UP,function(){
                $.post("/cmd","stop",function(data,status){
                });
            });
        });
         
    </script>
 
    <style type="text/css">
        button {
            margin: 10px 15px 10px 15px;
            width: 50px;
            height: 50px;
        }
        input {
            margin: 10px 15px 10px 15px;
            width: 50px;
            height: 50px;
        }
    </style>
     
</head>
<body>
<div id="container" class="container" align="center">
    <img width="320" height="240" src="http://192.168.10.130:8080/?action=stream"><br/>
    <table align="center">
        <tr>
            <td align="center"><b>Motor Contrl</b></td>
            <td align="center"><b>Servo Contrl</b></td>
        </tr>
        <tr>
            <td>
                <div align="center">
                    <button id="forward" class="btn btn-lg btn-primary glyphicon glyphicon-circle-arrow-up"></button>
                </div>
                <div align="center">
                    <button id='turnleft' class="btn btn-lg btn-primary glyphicon glyphicon-circle-arrow-left"></button>
                    <button id='turnright' class="btn btn-lg btn-primary glyphicon glyphicon-circle-arrow-right"></button>
                </div>
                <div align="center">
                    <button id='backward' class="btn btn-lg btn-primary glyphicon glyphicon-circle-arrow-down"></button>
                </div>
            </td>
            <td>
                <div align="center">
                    <button id="up" class="btn btn-lg btn-primary glyphicon glyphicon-circle-arrow-up"></button>
                </div>
                <div align="center">
                    <button id='left' class="btn btn-lg btn-primary glyphicon glyphicon-circle-arrow-left"></button>
                    <!--<button id='stop' class="btn btn-lg btn-primary glyphicon glyphicon-stop"></button>-->
                    <button id='right' class="btn btn-lg btn-primary glyphicon glyphicon-circle-arrow-right"></button>
                </div align="center">
                <div align="center">
                    <button id='down' class="btn btn-lg btn-primary glyphicon glyphicon-circle-arrow-down"></button>
                </div>
            </td>
        </tr>
    </table>
    <input type="range" min="0.0" max="100.0", style="width:300px";>
</div>
</body>
</html>

分析:

头文件中通过网页链接引入一个css样式文件,以及jquery.js文件。 脚本中首先判断是移动端还是PC端。如果是移动端则注册"touchstart",“touchend”事件,如果是PC端则注册“mousedown”,mouseup事件。 当按键按下时,发送按键的id号到/cmd。当按键释放时,发送停止命令“stop”到/cmd。 网页主体中通过引入mjpgs-streamer的链接引入视频窗口。设置图像大小为高240,宽320。 <img width="320" height="240" src="http://192.168.10.130:8080/?action=stream"> 此ip地址以及端口号须改为树莓派实际的ip地址和端口号。