2013年5月23日木曜日

Python Pyramid + Raspberry Pi PWM

Raspberry Pi に Python Pyramid のウェブアプリをのせて、ウェブアプリから PWM の設定を行うというアプリをつくる。
まず、Mac 上に Pyramid の環境を作って、そこで動作の確認を行う。
次のインストールは Raspberry Pi 上ではなく、Mac に構築している。
できあがってから、ソースを Raspberry Pi へ移動することにする。

作成するのは、PWM 制御のウェブアプリ。
ウェブでスライダーを表示して、PWM のパルス幅を制御する。
実際に PWM の制御をするのは root 権限が必要なので、ウェブアプリとは別の Python アプリとする。
二つのアプリ間での情報伝達はテキストファイルにする。
  1. Pyramidのインストール
    virtualenvで環境を作って、そこにPyramidをインストールする。
    virtualenv と virtualenvwrapper をすでにインストールしてある場合は、3行目の env_pwm をつくるところから。
    $ sudo pip install virtualenv
    $ sudo pip install virtualenvwrapper
    $ virtualenv --no-site-packages env_pwm
    $ cd env_pwm
    $ bin/pip install pyramid
    

  2. pwm という名前の Pyramid Project の作成
    最終的に、ログインパスワードを入力させてからページを表示したいので、starter ではなく、alchemy とします。
    $ bin/pcreate -s alchemy PyPWM
    

  3. つくった PyPWM プロジェクトを動かしてみる
    setup.py は必要なモジュールを自動的にインストールするので、実行を終了するまで時間がかかる場合があります。
    $ cd PyPWM
    $ ../bin/python setup.py develop
    $ ../bin/python setup.py test -q
    $ ../bin/pserve development.ini
    

    ここで、localhost:6543 を見に行くとエラーが表示されています。
    こんな感じ。
    Pyramid is having a problem using your SQL database.  The problem
    might be caused by one of the following things:
    

    これをやれ、ということで・・・。
    $ ../bin/initialize_PyPWM_db development.ini
    

    もう一度・・・。
    $ ../bin/pserve development.ini
    

    で、localhost:6543 を見に行くと、今度はちゃんと表示されています。
    この段階のプロジェクトを bitbucket にあげておきます。

    ついでに wsgi の設定もしてしまいます。
    Mac の場合は/etc/apache2/other の modwsgi.conf をこのようにします。
    WSGIApplicationGroup %{GLOBAL}
    WSGIPassAuthorization On
    WSGIDaemonProcess pyramid user=snaf group=staff threads=4 \
       python-path=/Users/pi/env_pwm/lib/python2.7/site-packages
    WSGIScriptAlias /pwm /Users/pi/env_pwm/pyramid.wsgi
    
    <directory env_pwm="" pi="" sers="">
      WSGIProcessGroup pyramid
      Order allow,deny
      Allow from all
    </directory>
    

    /Users/pi/env_pwm/pyramid.wsgi はこう。
    from pyramid.paster import get_app, setup_logging
    ini_path = '/Users/pi/env_pwm/PyPWM/production.ini'
    setup_logging(ini_path)
    application = get_app(ini_path, 'main')
    

    これで、Mac 上の localhost/pwm で PyPWM のページが表示されます。

    Raspberry Pi の wsgi も設定変更しておきます。
    /home/pi/env_pwm/pyramid.wsgi
    from pyramid.paster import get_app, setup_logging
    ini_path = '/home/pi/env_pwm/PyPWM/production.ini'
    setup_logging(ini_path)
    application = get_app(ini_path, 'main')
    

    /etc/apache2/mods-available/wsgi.conf の最後の方に追加したもの。
    WSGIApplicationGroup %{GLOBAL}
    WSGIPassAuthorization On
    WSGIDaemonProcess pyramid user=pi group=pi threads=4 \
       python-path=/home/pi/env_pwm/lib/python2.7/site-packages
    WSGIScriptAlias /pwm /home/pi/env_pwm/pyramid.wsgi
    
    <Directory /home/pi/env_pwm>
      WSGIProcessGroup pyramid
      Order allow,deny
      Allow from all
    </Directory>
    

  4. 忘れずに Raspberry Pi に mercurial をインストールしておく
    Mac で作成したプロジェクトを Bitbucket にあげておいて、Raspberry Pi 側でそれを持ってこようと言う考え。hg ではなくて git をお使いの方はそちらをどうぞ。
    $ sudo apt-get install mercurial
    

    Raspberry Pi 側では、clone する前に Mac でやったのと同じように Python の仮想環境を作って Pyramid をインストールし、 PyPWM プロジェクトをつくって実行してみる、というところまではやっておく必要があります。

  5. Mac 上の Eclipse で Pyramid の PyPWM プロジェクトを作成したディレクトリに、同じ名前で Eclipse のプロジェクトを作成
  6. Python へのパスは、その仮想環境の Python とします。env_pwm/bin/python です。
    Python Pyramid で「Eclipseを使う」を参照してください。

  7. development.ini の修正
    「sqlalchemy.url = sqlite:///...」の上に、これを追加する。
    development.ini だけではなく、production.ini にも追加しておく。
    # mako template settings
    mako.directories = pypwm:templates
    

    file log の設定を handlers と logger_root に追加する。handler_filelog も追加する。
    [handlers]
    keys = console, filelog
    
    [logger_root]
    level = INFO
    handlers = console, filelog
    
    [handler_filelog]
    class = FileHandler
    args = ('%(here)s/pypwm.log','a')
    level = NOTSET
    formatter = generic
    

    古い jQuery を読み込もうとするので、pyramid_debugtoolbar をコメントアウトする。

  8. pypwm/views.py の修正
    念のためにこれを最初に置く。
    #!/usr/bin/env python
    # coding: UTF-8
    

    Mac OS X と Raspberry Pi の切り替えのためにこれを追加する。初期設定ファイルの場所。
    import os
    os_name = os.uname()
    if os_name[0] == 'Darwin':
        fname = '/Users/pi/env_pwm/PyPWM/pypwm/pwm.prefs'
    elif os_name[0] == 'Linux':
        fname = '/home/pi/env_pwm/PyPWM/pypwm/pwm.prefs'

  9. pypwm/__init__.py の修正
    main() にadd_router と add_renderer を追加する。
    add_renderer は、こうすることで、mako テンプレートの拡張子を html とすることができるようになる。
    def main(global_config, **settings):
        """ This function returns a Pyramid WSGI application.
        """
        engine = engine_from_config(settings, 'sqlalchemy.')
        DBSession.configure(bind=engine)
        Base.metadata.bind = engine
        config = Configurator(settings=settings)
        config.add_static_view('static', 'static', cache_max_age=3600)
        config.add_route('home', '/')
        config.add_route('home_org', '/home_org')
        config.add_route('pypwm', '/pypwm')
        config.add_route('update_slider_pwm', '/pypwm/update_slider_pwm')
        config.add_renderer(".html", "pyramid.mako_templating.renderer_factory")  
        config.scan()
        return config.make_wsgi_app()
    
  10. ひとつひとつは大変なので
    こんな感じにファイルを置きます。
    jQuery Mobile を使うので、関連ファイルをstatic ディレクトリに置いてあります。
    ほかには、templates/ に index.html があります。


  11. py_pwm.py
    8 ch 分になっているが、web アプリ側は 1 ch のみ。
    #!/usr/bin/env python
    # coding: UTF-8
    
    """
    py_pwm.py
    
    RPIO を使うので、py_pwm.py は root 権限で実行する必要がある。
    crontab で起動時に立ち上がるように設定しておく。
    prefs を 0.1 秒ごとに読んで、更新されていれば、PWM の値をその値で更新する。
    prefs が無ければ、0% の出力とする。
    prefs は、web アプリ側で作成および書き込みを行う。
    prefs の値は ch0 から ch7 までの8つの値を「,」(カンマ)で区切って並べたものとする。
    prefs に書き込まれている値は、0から 1999 までの整数値とする。
    prefs に書き込まれている値を10倍したマイクロ秒の値がパルス幅となる。
    prefs の場所はパラメタで指定する。
    
    @reboot /home/pi/env_pwm/PyPWM/run_as_root/py_pwm.py /home/pi/env_pwm/PyPWM/pypwm/pwm.prefs
    """
    import sys, string, time, os
    os_name = os.uname()
    if os_name[0] == 'Darwin':
        pass
    elif os_name[0] == 'Linux':
        if os_name[1] == 'raspberrypi':
            from RPIO import PWM
    
    class PyPwm():
        my_debug = False
        kChNum = 8
        kMinVal = 0
        kMaxVal = 1999
        prefs = 'pwm.prefs'
        pwm_vals = []
        pwm_out = None
        pwm_init  = [ 0,  0,  0,  0,  0,  0,  0,  0]
        pwm_gpios = [17, 18, 27, 22, 23, 24, 25,  4]
        pwm_pins  = [11, 12, 13, 15, 16, 18, 22,  7]
        prev_vals = [ 0,  0,  0,  0,  0,  0,  0,  0]
        
        def __init__(self, prefs):
            self.prefs = prefs
            for ii in range(self.kChNum):
                init = self.pwm_init[ii]
                self.pwm_vals.append(init)
            if os_name[0] == 'Linux':
                self.pwm_out = PWM.Servo()
        
        
        def read_prefs(self):
            fd = None
            try:
                fd = open(self.prefs, 'r')
            except IOError:
                fd = None
            if fd is not None:
                line = fd.readline()
                
                if self.my_debug:
                    print ("read_prefs: %s" % (line))
                    
                items = line.split(',')
                ii = 0
                for item in items:
                    item = string.strip(item)
                    if ii < self.kChNum:
                        try:
                            item = int(item)
                        except ValueError:
                            item = 0
                        if item < self.kMinVal:
                            item = self.kMinVal
                        if item > self.kMaxVal:
                            item = self.kMaxVal
                        self.pwm_vals[ii] = item
                    ii += 1
                
            
        def main(self):        
            self.read_prefs()
            
            if self.my_debug:
                for item in self.pwm_vals:
                    print item
            
            while True:
                for ii in range(self.kChNum):
                    gpio = self.pwm_gpios[ii]
                    val = self.pwm_vals[ii]
                    val *= 10
                    if val != self.prev_vals[ii]:
                        self.prev_vals[ii] = val
                        if self.pwm_out is not None:
                            if val > 0:
                                self.pwm_out.set_servo(gpio, val)
                                if self.my_debug:
                                    print "set_servo gpio = %2d, %5d" % (gpio, val)
                            elif val == 0:
                                self.pwm_out.stop_servo(gpio)
                                if self.my_debug:
                                    print "stop_servo gpio = %2d" % (gpio)
                time.sleep(0.1)
                self.read_prefs()
            
            
        
    if __name__ == '__main__':
        argv = sys.argv
        argc = len(argv)
        if argc >= 2:
            fname = argv[1]
        else:
            fname = 'pwm.prefs'
        
        py_pwm = PyPwm(fname)
        py_pwm.main()
    

  12. ajax_slider_pwm.js
    // # coding: UTF-8
    // ajax_slider_pwm.js
    
    function onchange_slider_pwm() {
     onchange_slider_sub(1, '#slider_pwm');
    }
    
    function onchange_slider_sub(channel, slider_id) {
     var slider_val = jQuery(slider_id).val();
     send_option_request(slider_val, channel, 'write');
    }
    
    function send_option_request(slider_val, channel, command) {
     // command is 'read' or 'write'
        $("#id_loading").text("Loading...");
     var the_url = "update_slider_pwm";
     if (window.location.pathname != "/") {
      the_url = window.location.pathname + "/" + the_url;
     }
     // alert("the_url = " + the_url);
        $("#id_debug").empty();
        $.ajax({
            dataType: "json",
            data: {
                "pwm_val": slider_val,
                "channel": channel,
                "command": command
            },
            cache: true,
            url: the_url,
            type: "get",
            success: function (data, dataType) {
                // alert("success: " + data);
                resp_pwm = rcv_response(data); // resp has channel and pwm_val
                $("#id_loading").empty();
                if (command == "read") {
                 var slider_id = "#slider_" + resp_pwm.channel
                 jQuery(slider_id).val(resp_pwm.pwm_val);
                 $("#id_debug").text(resp_pwm.pwm_val)
                }
            },
            error: function(XMLHttpRequest, textStatus, errorThrown) {
                $("#id_debug").text("ajax error");
            }
        });
    }   // function send_option_request(which, key)
    
    
    function rcv_response(jd) {
     resp_pwm = {pwm_val:jd.pwm, channel:jd.channel, command:jd.command};
     return resp_pwm;
    }   // function rcv_option_response()

  13. index.html
    ## for maco
    <!DOCTYPE html>
    <html>
    <head>
      <title>PyPWM</title>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
      <meta name="apple-mobile-web-app-capable" content="yes" />  
      <meta name="keywords" content="python web application" />
      <meta name="description" content="pyramid web application" />
      <link rel="shortcut icon" href="${request.static_url('pypwm:static/favicon.ico')}" />
      <link rel="stylesheet" href="${request.static_url('pypwm:static/jquery/jquery.mobile-1.3.1.min.css')}" />
      <link rel="stylesheet" href="${request.static_url('pypwm:static/jquery/slider.css')}" />
      <script src="${request.static_url('pypwm:static/jquery/ajax_slider_pwm.js')}"></script>
      <script src="${request.static_url('pypwm:static/jquery/jquery-1.9.1.min.js')}"></script>
      <script src="${request.static_url('pypwm:static/jquery/jquery.mobile-1.3.1.min.js')}"></script>
      <style type="text/css"></style>
    </head>
    <body>
    
    <form action="${request.route_url('home')}" method="post">
    <div id="slider">
      <label for="slider_pwm">PWM:</label>
      <input type="range" name="slider_pwm" id="slider_pwm" value="${pwm_dict['pwm_val']}" min="0" max="1999" step="1" onchange="onchange_slider_pwm()" />
    </div>
    </form>
    
    <div id="id_loading"></div>
    <div id="id_debug"></div>
    </body>
    </html>

  14. __init__.py
    from pyramid.config import Configurator
    from sqlalchemy import engine_from_config
    
    from .models import (
        DBSession,
        Base,
        )
    
    
    def main(global_config, **settings):
        """ This function returns a Pyramid WSGI application.
        """
        engine = engine_from_config(settings, 'sqlalchemy.')
        DBSession.configure(bind=engine)
        Base.metadata.bind = engine
        config = Configurator(settings=settings)
        config.add_static_view('static', 'static', cache_max_age=3600)
        config.add_route('home', '/')
        config.add_route('home_org', '/home_org')
        config.add_route('update_slider_pwm', '/update_slider_pwm')
        config.add_renderer(".html", "pyramid.mako_templating.renderer_factory")  
        config.scan()
        return config.make_wsgi_app()
    

  15. view.py
    #!/usr/bin/env python
    # coding: UTF-8
    from pyramid.response import Response
    from pyramid.view import view_config
    
    from sqlalchemy.exc import DBAPIError
    
    from .models import (
        DBSession,
        MyModel,
        )
    
    import time, string, os
    os_name = os.uname()
    if os_name[0] == 'Darwin':
        fname = '/Users/pi/env_pwm/PyPWM/pypwm/pwm.prefs'
    elif os_name[0] == 'Linux':
        fname = '/home/pi/env_pwm/PyPWM/pypwm/pwm.prefs'
    
    @view_config(route_name='home_org', renderer='templates/mytemplate.pt')
    def my_view(request):
        try:
            one = DBSession.query(MyModel).filter(MyModel.name == 'one').first()
        except DBAPIError:
            return Response(conn_err_msg, content_type='text/plain', status_int=500)
        return {'one': one, 'project': 'PyPWM'}
    
    conn_err_msg = """\
    Pyramid is having a problem using your SQL database.  The problem
    might be caused by one of the following things:
    
    1.  You may need to run the "initialize_PyPWM_db" script
        to initialize your database tables.  Check your virtual 
        environment's "bin" directory for this script and try to run it.
    
    2.  Your database server may not be running.  Check that the
        database server referred to by the "sqlalchemy.url" setting in
        your "development.ini" file is running.
    
    After you fix the problem, please restart the Pyramid application to
    try it again.
    """
    
    
    #---- pwm
    @view_config(route_name='home', renderer='index.html')
    def home(request):
        pwm_dict = {}
        pwm_val = read_prefs()
        pwm_dict['pwm_val'] = pwm_val
        return dict(pwm_dict=pwm_dict)
    
    
    @view_config(route_name='update_slider_pwm', xhr=True, renderer='json')
    def update_slider_pwm(request):
        """
        This function is call from ajax_slider_pwm.js send_option_request when slider is updated.
        receive json pwm data by ajax
                "pwm_val": slider_val,
                "channel": channel,
                "command": command
        command is 'read' or 'write'
        """
        pwm_val = request.GET.get('pwm_val')
        pwm_val = to_int(pwm_val)
        channel = request.GET.get('channel')
        channel = to_int(channel)
        command = request.GET.get('command')
        if command == 'read':
            pwm_val = read_prefs()
        elif command == 'write':
            write_prefs(pwm_val)
        return {
            'pwm_val': pwm_val,
            'channel': channel,
            'command': command,
        }
    
    
    def read_prefs():
        kMinVal = 0
        kMaxVal = 1999
        pwm_val = 0
    
        fd = None
        try:
            fd = open(fname, 'r')
        except IOError:
            fd = None
        if fd is not None:
            line = fd.readline()
            pwm_val = string.strip(line)
            try:
                pwm_val = int(pwm_val)
            except ValueError:
                pwm_val = 0
            if pwm_val < kMinVal:
                pwm_val = kMinVal
            if pwm_val > kMaxVal:
                pwm_val = kMaxVal
            fd.close()
        return pwm_val
    
    
    def write_prefs(pwm_val):
        my_error_count = 0
        fin = False
        while (not fin):
            try:
                fd = open(fname, 'w')
            except IOError:
                fd = None
            if fd is not None:
                fd.write(str(pwm_val))
                fd.close()
                fin = True
            else:
                if my_error_count < 10:
                    my_error_count += 1
                    time.sleep(0.1)
                else:
                    fin = True
    
    def to_int(the_str):
        try:
            int_value = int(the_str)
        except:
            int_value = 0
        return int_value
    

こんな感じで表示される。

2 件のコメント: