Reactのホットデプロイ環境を作る

昨今のトランスパイルやバンドル作業が行われるWEBフロント開発では書いたコードがそのままブラウザで動かないために、WEBブラウザで実行確認するためにはビルド・ブラウザリロードの手間が必要です。ホットデプロイ環境を構築すると、コードが書き変えを監視していて必要時に自動的にビルドとブラウザリロードがなされて最新の状態を見ることができるようになります。webpackではホットデプロイ用途にwebpack-dev-serverが用意されていて、その構成や設定は結構シンプルになっています。

ES6のホットデプロイ

$ npm install -save-dev webpack-dev-server

npmで取ってきたら最新安定バージョンは2.7.1でした。

/* webpack.config.babel.js */
import path from 'path';

export default {
    entry: './js/App.jsx',
    output: {
        path: path.resolve('static/js'),
        filename: 'bundle.js',
        /* バンドルファイル配置の調整のために追加 */
        publicPath: '/js/',
    },
    devtool: 'source-map',
    module: {
        rules: [{
            test: /\.(js|jsx)$/,
            exclude: /node_modules/,
            use: ['babel-loader', 'eslint-loader'],
        }],
    },
    resolve: {
        extensions: ['.js', '.jsx'],
    },
    devServer: {
        /* 静的コンテンツ配置の調整のために追加 */
        contentBase: path.resolve('static'), 
        /* APIサーバのポート競合回避のために追加 */
        port: 9000,
        /* APIサーバをプロクシするために追加 */
        proxy: {
            '/data': {
                target: 'http://localhost:8080',
                secure: false,
            },
        },
    },
};

プロジェクトの構成がwebpack-dev-serverを意識した作りにしていればnpmインストールだけで何も設定せずとも動くのですが、私の場合は既存のプロジェクトが以下の調整を必要としました。

  • バンドルされたJSコードが、サーバルートではなく、/js/bundle.js と一階層下に配置していた。outputエントリの中に「publicPath」設定で/js/をURLに挿入する。
  • 静的コンテンツが「static」フォルダに配置してあり、URLのマッピングが必要だった。devServerエントリの中に、「contentBase」設定で/static/をコンテンツルートに設定する。
  • APIサーバがあり、ポート8080番で動いているのでwebpack-dev-serverとそのままではポート競合する。devServerエントリの中に、「port」設定でとりあえず9000番に設定した。さらにwebpack-dev-serverで受けたAPIリクエストをAPIサーバにそのまま渡すためにプロクシの設定を行う。「proxy」設定で転送先の情報を記述する。

Reactのホットデプロイ

上記までで、ES6を用いたWEBフロントのホットデプロイ環境は整います。私はdirenvを入れて/node_module/.binにパスを通しているのでおもむろに

$ webpack-dev-server --hot --open

でターミナルから起動すればビルドが走り、自動でWEBブラウザも開きます。npmスクリプト設定にも書いておきました。 --openスイッチはブラウザを開くかどうかだけなので付けなくても支障ない。

{
  "scripts": {
    "build": "rm ./static/js/*; webpack",
    "start": "webpack-dev-server --hot",
    "test": "jest"
  },
}

しかし、これだけでは普通のアプリならいいらしいけど、私のReact使ったアプリではウンともすんとも言わない。Reactのアプリケーションはさらに設定を重ねてReactにもホットデプロイの特別な環境を上乗せしないといけません。バーチャルDOM使ってる系はダメなんだろうね。

$ npm install -save react-hot-loader
$ npm install -save-dev babel-polyfill 
  • 必要なものをnpmでインストールします。アプリケーションを書き換える必要があり、その際にeslintが怒るのでreact-hot-loaderはdependenciesのほうに入れます(-saveスイッチ)。
/* webpack.config.babel.js */
import path from 'path';

export default {
    /* Reactホットデプロイのためにビルド対象に追加 */
    entry: [
        'babel-polyfill',
        'react-hot-loader/patch',
        './js/App.jsx',
    ],
    output: {
        path: path.resolve('static/js'),
        filename: 'bundle.js',
        publicPath: '/js/',
    },
    devtool: 'source-map',
    module: {
        rules: [{
            test: /\.(js|jsx)$/,
            exclude: /node_modules/,
            use: [
                /* これまで共有していたBabel設定をeslintやJestに影響しないようにここに書く */
                {
                    loader: 'babel-loader',
                    options: {
                        presets: [
                            ['es2015', { modules: false }],
                            'react',
                            'stage-1',
                        ],
                        plugins: [
                            'react-hot-loader/babel',
                            'transform-decorators-legacy',
                        ],
                    },
                },
                'eslint-loader',
            ],
        }],
    },
    resolve: {
        extensions: ['.js', '.jsx'],
    },
    devServer: {
        contentBase: path.resolve('static'),
        port: 9000,
        proxy: {
            '/data': {
                target: 'http://localhost:8080',
                secure: false,
            },
        },
    },
};
  • webpack.config.babel.jsでビルド対象に、babel-polyfillとreact-hot-loader/patchを追加します。ちょっと理由がよくわかってなくておまじない的ですが、いろいろ抜き差しして試してみた結果、これら3つを並べるのが最低要件。
  • Babelの設定はeslintやJestとも共有していて、package.jsonに書いてましたが、今回のReactホットデプロイのために設定を変えないといけないので、他に影響しないようにwebpack.config.babel.jsのほうに上書き設定します。この場合、差分設定だけでなくフルに書かないといけない。es2015プリセットでmoduleをfalseにしているのはwebpack3だからです。これと、react-hot-loader/babelプラグインを足すのが必要要件。
// App.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';

// import Root from './Root';
import Root from './Root2';

const render = () => {
    ReactDOM.render(
        <AppContainer><Root /></AppContainer>, 
        document.getElementById('application'),
    );
};

render();

if (module.hot) {
    // module.hot.accept('./Root', () => { render(); });
    module.hot.accept('./Root2', () => { render(); });
}
  • アプリケーションのメインルーチンのところでコードを書き換える必要があります。react-hot-loaderのAppContainerコンポーネントで元のアプリを囲うのと、module.hot関連のこちらもおまじない的コード。

これでReactのアプリがコード書き換えに即時反映するようになりました。

// Root2.jsx: 非react-router3アプリとしてホットデプロイ確認用。
import React from 'react';

export default () => (
    <div>Hello React Hot Loader!</div>
);

こちらのdivタグTEXTを書き換えて保存すると、ビルドが自動で走り、ブラウザも自動でリロードされてホットデプロイが確認できます。ここまでで大概の場合はOK。ちゃんと動く。

しかし大問題。

シンプルなReactアプリではこれでOKですが、私のはダメ。react-router 3.xを使ったものはウンともすんとも言わなかった。先の例ではRoot2.jsxを使ってますが本来のコードは下記のようなものです。バリバリreact-router3かつredux。

// Root.jsx: react-router3アプリ
import React from 'react';
import injectTapEventPlugin from 'react-tap-event-plugin';
import { createStore, combineReducers, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import { Router, Route, browserHistory, IndexRedirect } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
import { reducer } from 'redux-form';

import { BookList, BookReader, NotFound, Navigation } from './containers';
import * as reducers from './reducers';
import cookiesMiddleware from './middlewares/CookiesMiddleware';

injectTapEventPlugin();

const store = createStore(
    combineReducers({ ...reducers, form: reducer }),
    applyMiddleware(cookiesMiddleware()),
);

export default () => (
    <Provider store={store}>
        <Router history={syncHistoryWithStore(browserHistory, store)}>
            <Route path="/" component={Navigation}>
                <IndexRedirect to="list" />
                <Route path="list" component={BookList} />
                <Route path=":title/:vol/:page" component={BookReader} />
                <Route path="*" component={NotFound} />
            </Route>
        </Router>
    </Provider>
);

react-router 4.xにしないといけない。react-router 4にあげていなかった理由はreact-router-reduxがベータ版のためでしたが、react-router-reduxは実装コード量がとても少ないライブラリなのでなんかあっても読めばいいかと。重い腰あげてマイグレーション作業をやるとしますか。

github.com

react-router 3のバージョン問題は、しっかりreact-hot-loaderのドキュメントにKnown limitationsとして書かれていた。「React Router v3 is not fully supported 」どころではなく、私の場合は全くホットじゃない。

実行速度

さて、ホットデプロイの環境で気になるのは、バックグラウンドでコード書き換えを監視して自動ビルドを行うわけですから、実行速度やマシンへの圧迫度です。私のMBP15(Mid 2015)メモリ16GBは今ならちょっと古いぐらいですけど、ビルドしてるからって重くなったりとかまったくないです。webpack-dev-serverを走らせると、WEBブラウザが強力にバンドルJSをキャッシュすることがなくなるので、実はホットじゃなくても便利。

しばらく見ない間にES6開発ツールセットが良くなってた

WEBのフロントエンドをES6でreact + redux + react-router + Material-UI利用して書いてますが、このビルド環境をそれまでのGulp + Browserifyからnpm + webpackに置き換えました。しばらく見ない間にツール毎の進化があって組み合わせがシンプル化できるようになってきています。WEBフロントの開発ツールエコシステムについてES6キャズムを超えたんじゃないですかね?ライブラリやツール自体もES6で書かれているもの多いですし、雑多に広がる感じから安定成長になってきた印象を受けます。

package.json のscriptsとdevDependenciesの抜粋については以下の通り。eslintはairbnbの最新バージョンで動く組み合わせを探ったら3.xバージョンになりました。4.xだとルールに競合が出てましたが、そのうち成熟するでしょう。それ以外は今日時点での最新バージョンでそろってます。npmは5.3.0、nodeは8.4.0にバージョンアップしました。

  "scripts": {
    "build": "rm ./static/js/*; webpack",
    "test": "jest"
  },
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-eslint": "^7.2.3",
    "babel-jest": "^20.0.3",
    "babel-loader": "^7.1.1",
    "babel-plugin-transform-decorators-legacy": "^1.3.4",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-react": "^6.24.1",
    "babel-preset-stage-1": "^6.24.1",
    "eslint": "^3.19.0",
    "eslint-config-airbnb": "^15.1.0",
    "eslint-loader": "^1.9.0",
    "eslint-plugin-import": "^2.7.0",
    "eslint-plugin-jsx-a11y": "^5.1.1",
    "eslint-plugin-react": "^7.2.1",
    "jest": "^20.0.4",
    "webpack": "^3.5.5"
  },

npmの実行スクリプトで、buildとtestを行いますがそれぞれwebpackとjestを呼ぶだけ。Webpackでバンドル、eslintでAirbnbルールのチェック、Jestでテストをします。それぞれトランスパイルが必要なのでBabelを設定しています。

Babel

設定をなるべく一箇所にまとめたいのでBabelの設定もpackage.jsonに。package.jsonの該当箇所を抜粋。

  "babel": {
    "presets": [
      "es2015",
      "react",
      "stage-1"
    ],
    "plugins": [
      "transform-decorators-legacy"
    ]
  },

プリセットのes2015とreactは普通。stage-1は展開演算子「…」を、transform-decorators-legacyはJS-classに前置する@デコレータ(Javaアノテーションみたいなやつで、redux-formの利用で出てくる)を解釈するために入れています。package.jsonにきちんと書いておけばその他のツールにBabelの設定は必要なく、設定を使いまわしてくれます。

eslint

eslintの設定抜粋は以下のとおり。package.jsonより。

  "eslintConfig": {
    "extends": "airbnb",
    "env": {
      "browser": true,
      "jest": true
    },
    "parser": "babel-eslint",
    "rules": {
      "indent": ["warn", 4, {"SwitchCase": 1}],
      "max-len": ["warn", 120],
      "react/jsx-indent": ["warn", 4],
      "react/jsx-indent-props": ["warn", 4]
    }
  },

AirbnbのルールカスタマイズはWebStormのエディタの初期値にあわせてのインデント幅調整だけです。前はいろいろいじらないと無理があったのですが、最近は私の手が慣れたのか、Airbnbルールが現実的になってきたのか、ストレスなくコード書けるようになりました。envエントリーで、jestをtrueにしてるのは、グローバルスコープのJestが提供するメソッドをインポートなく用いた際にLintで引っ掛けないために必要です。サーバサイドはGoで書いているのでnodeはfalseでOK。

Jest

facebook.github.io

Jasmineを用いてましたが、なんかWebStormがJestにがっちり対応していたので乗り換え。2年ほど前はそもそもJestを動くところまで設定するのに試行錯誤があったりして未成熟感丸出しだったところが、いまはこなれて安定感満点。同期テストはもちろん問題なく、非同期テストはAxiosでGo製APIサーバとの通信コードぐらいしか私のプロジェクトではやるところないのだけどJasmineですでに不便なかったのと同様にまったく問題ない。

Jestの設定もpackage.jsonに行います。

  "jest": {
    "transform": {
      "\\.(js|jsx)$": "babel-jest"
    }
  },

なんか新旧いろんなドキュメントにいろんなやり方が書いてあるのですが、今の20.0バージョンではこれが最もシンプルなんじゃないかな。AirbnbでJSXは.jsxに書かないと怒られるのでトランスパイル対象を.jsおよび.jsxに正規表現でマッチさせます。ちょっと違和感あるのは正規表現エスケープであるバックスラッシュをエスケープするためにもういっちょバックスラッシュを重ねてるところ。JSプロパティ名にコンパイル前の正規表現文字列を書くなんて、こんなパターン初めて見た。値には橋渡しするbabel-jestを記述します。さて環境の動作確認として動作するコードのテスト形式な実行をしてみましょう。

import { Map } from 'immutable';

describe('An usage of Immutable.js', () => {
    test('Set and Merge values', () => {
        const state1 = Map({ one: 0, two: 0 });
        const state2 = state1.set('one', 1).merge({ two: 2, three: 3 });
        expect(state1.get('one')).toBe(0);
        expect(state1.get('two')).toBe(0);
        expect(state2.get('one')).toBe(1);
        expect(state2.get('two')).toBe(2);
        expect(state2.toJS()).toEqual({ one: 1, two: 2, three: 3 });
    });
});

test('Toggle bool value', () => {
    const state1 = Map({ value: false });
    const state2 = state1.update('value', value => !value);
    expect(state1.get('value')).toBeFalsy();
    expect(state2.get('value')).toBeTruthy();
});

importがきっちり効くからプロジェクト内のソースをどこからでも引っ張ってきて、かつ、ES6でテスト書けるし。Jasmine以前より様々なテストツールがそうだったからdescribeでまとめてるけど、まとめないでいきなりtest関数書いてもOK。デフォルトで、テストファイルは「__tests__」という名前のフォルダに置くか、/\.(test|spec)\.jsx?$/なファイル名をきっかけとして、検索し全部実行してくれます。

f:id:masataka_k:20170816071340p:plain

WebStormでUI対応しているし。しかしデバッガーで止まらない。なんかまだ足りないのかな?単体テストをデバッガーで止められると細かくややこしいところを調査・修正するのに助かるのになあ。

webpack

webpackの設定は、package.jsonにいっしょに書けない。ほとんどJSON返すだけなので書けてもいいとおもうんだけどな。

/* webpack.config.babel.js */
import path from 'path';

export default {
    entry: './js/App.jsx',
    output: {
        path: path.resolve('static/js'),
        filename: 'bundle.js',
    },
    devtool: 'source-map',
    module: {
        rules: [{
            test: /\.(js|jsx)$/,
            exclude: /node_modules/,
            use: ['babel-loader', 'eslint-loader'],
        }],
    },
    resolve: {
        extensions: ['.js', '.jsx'],
    },
};

特に難しいことはしてません。唯一のハマりポイントはresolveエントリを追加して、わざわざ.jsと.jsx拡張子に反応するよう書かないといけないこと。webpack3からの変更?2以前の解説ドキュメントでは必要とされていない。babelとeslintの設定はpackage.jsonをちゃんと参照してくれています。

direnv

プロジェクトルートにある、.envrcの内容は以下の通り。

export PWD="`pwd`"
export GOPATH=$PWD/vendor:$PWD
export PATH=$PWD/node_modules/.bin:$PWD/bin:$PATH

direnvをインストールしてあるので、コンソールを開いてすぐにJSツールのパスを通すことが楽チン。これでWebStormのTerminalからいきなり、jestとかwebpackとか打ち込んでもツールが起動する。npm install -gでグローバルに環境構築をしなくても、これで同じ効果が得られる。同居するAPIサーバコードのためにGOPATHも設定しています。

まだやってないこと

  • webpack-dev-serverを走らせてホットデプロイな環境にしていないので、ブラウザのキャッシュが効きまくってコードの変更が反映されない。
  • Immutable.jsをテストコードでつかってるのは、このあとReduxのreducerをImmutable.jsを用いて「…」展開演算子をなくすように書き直そうと思ってるため。まだやってない。
  • webpackでのビルド時にJestを走らせるようにはまだしていない。
  • Jestを用いる肝心要はReactの描画について単体テストすること。それはまだやってない。前にちょっとだけJasmine+JsDomでやってて苦痛だったのを書き直し、よければカバレッジ上がるように新しくテストを書き足す予定。
  • webpackでソースマップ出力をしていますが、まったく活用できてない。
  • 書いてるうちにcssが必要無くなったので、webpackでsassコンパイル&埋め込みをまだしていない。グリッドレイアウトを行うのにBootstrapのsassライブラリを持ってくる段になったらやる。

沢山やってないことがあるけど、前のGulp + Browserifyの環境よりも格段に構成や設定がシンプルになりました。

react-routerが都合よくAPIを公開してくれた!(がダメだった)

mk.hatenablog.com

かなり前ですけど、react-routerのURLをreduxで管理していい感じ、というコード書きました。こちらはreact-routerの非公開関数を利用していたので将来危険だなあと思ってましたが、4.xでやっぱりダメになりました。しかしそれは良い方向の変更で、利用していた非公開関数の同等機能をきちんとAPIにしてくれてたのです。イシューまでは読んでませんが、私のようなことをしてた人の要望だったのかも。

react-router/matchPath.md at master · ReactTraining/react-router · GitHub

利用するのは上記リンク先ドキュメントのmatchPathというAPIです。ちなみにリンク先ドキュメントは今日時点でほとんど何も書かれていないので、直に実装コードを読まないと使い方わかりません。

該当箇所は以下のとおりに元記事のReducer内のヘルパ関数の中身を書き直します。申し訳ないのは元記事ではtitleをURI内パラメータ、pageをクエリー値でもってくる仕様だったのを、title、vol、pageを全部URIエンコードされている仕様に変更してたわ。

import { matchPath } from 'react-router';

function decodeTitleAndPage(pathname) {
    const m = matchPath(pathname, { path: '/:title/:vol/:page' });
    if (!m) {
        return {};
    }
    let title = '';
    let vol = '';
    let page = 0;
    m.params.forEach((value, key) => {
        if (key === 'title') {
            title = value;
        } else if (key === 'vol') {
            vol = value;
        } else if (key === 'page') {
            page = Number(value);
        }
    });
    return { title, vol, page };
}

ダメだった

おっとどっこい。react-router-reduxがreact-routerの最新バージョンに追いついていない!動いていたみたいなのはブラウザのキャッシュが効いてただけでした。テストを走らせたら爆発して気づき、キャッシュクリアしたらちゃんとアプリも壊れました。そっと上の修正をもどして、react-routerも3.0.5にもどしました。

なにやらreact-router-reduxがreact-routerにマージされていくような様子?5.xでちゃんとするようなことが書いてあった。きちんとバージョン上がったときへの備忘録として記事は残しておきます。

4.xでなんで動かないか

ちょっと見てみた。

export const LOCATION_CHANGE = '@@router/LOCATION_CHANGE'

export function routerReducer(state = initialState, { type, payload } = {}) {
  if (type === LOCATION_CHANGE) {
    return { ...state, location: payload }
  }

  return state
}

react-router-reduxのrouterReducerのAction仕様が変わってる。反応するtypeの文字列が ‘@@router/LOCATION_CHANGE’ になってるし、パラメータ名が ‘location’ になってる。3.xではそれぞれ ‘locationChange’ と ‘locationBeforeTransitions’ 。まずこれだけで動くわけがない。

Make: Electronics ―作ってわかる電気と電子回路の基礎

Make: Electronics ―作ってわかる電気と電子回路の基礎 ((Make:PROJECTS))

Make: Electronics ―作ってわかる電気と電子回路の基礎 ((Make:PROJECTS))

入門用に電子回路ないし電子工作の分野の本を物色した結果、いつも大好きオライリーのこの本を購入しました。この種類の本は数多上梓されていますがこの本選んだ理由は、他がなんか貧乏くさい印象の紙面だったから。どうも電子工作分野の様々が全体的にチープ雰囲気漂ってる中で、紙面も装丁もきれいでいい感じだった。

読みながら手を動かすところあり、基本知識のコラムありで、もちろん内容も良いと思います。

Intel EditsonとRaspberry PI Camera

Intel Edison

f:id:masataka_k:20161121134937j:plain

やはり、小さいです。まずは標準アプリを母艦としたMBPに入れ初期設定しました。アプリでは、ファームアップデート・パスワード設定・WiFi設定の三つだけをやるもので、GUI操作だけで簡単にセットアップは終わります。動くところまで、Raspberry PI以上に超簡単。そこまでで週末に時間なかったので手をとめる。

Raspberry PI Camera

Amazon.com: Official Raspberry Pi 3 Case - Red/White: Computers & Accessories

Amazon.com: Raspberry Pi Camera Module V2 - 8 Megapixel,1080p: Computers & Accessories

f:id:masataka_k:20161121134718j:plain

Raspberry PIの方は、ケースとカメラを買ってみました。ケースは組み立てると完全に覆うものなのですが、壁の取り外しで基盤へのエントリもしやすくできてるなかなかの秀作です。そんな中、長女にねだられてRaspbianのフル版の方に入れ替え、モニタ・キーボード・マウスをつないでMineCraft PI Edition機になってしまいました。MineCraftはPI Editionだけプログラミング環境がくっついているらしく、Python操作できるらしい。いろいろWEBで調べながらコード書いて楽しんでます。カメラは。。。よって、つないで写真撮っただけで、通電もできず。

日本へ

明日から12月15日まで約3週間、日本出張です。Intel Edisonは週末用に持って行こうかと思います。Raspberryは占有されているので置いていかざるを得ません。

Ubuntu Core断念

Raspberry PIでサクッといっちゃったんで物足りずに、活用云々よりも先に違うLinuxディストロをいれてみようかと。で断念しました。

Snappy Ubuntu Core

Raspberry Pi 2/3 | Ubuntu developer portal

Raspberry PIの公式サイトでは公式の中の公式としてRaspbian(この名前はRaspberry + Debianなんでしょうけど、由来になんか違うものがあるんじゃないかと勘ぐってしまう)が掲げられていますけど、サードパーティWindowsなんかも載ってます。非デスクトップ用途で軽量に使えそうなLinuxということでは、Ubuntu Coreがありました。

これもダウンロードして、ddして、とRaspbianと同様なやり方でSDカードを用意します。。。がそこで問題発生。Ubuntu Coreは以下の理由からHeadlessで進まない。

  • デフォルトユーザーがなく、Ubuntu OneというクラウドサービスのIDが必要
  • そのUbuntu Oneと結びつけるためにはローカル実行、つまりディスプレイとキーボードをつなげてUbuntu Oneで登録したEメールアドレスを入力しないといけない
  • Ubuntu OneにID登録し、公開鍵登録し、ディスプレイとキーボードをつなげて作業をしたところ、どういうわけか認証がローカルもSSH越しもどちらも通らない
  • 試行錯誤するも通すことできず断念

セキュリティの都合なのか、クラウドサービスにユーザーを集めたかったからなのか、無いものは無いとしてしょうがないのですが、他のようにubuntu/ubuntuを用意しておいてくれてさえいれば困難もなかっただろうに。ということで、Raspbianに戻しました。

Raspberry PI ケースとカメラ

Raspberry PIは専用ケースをAmazon.comでポチって収納しました。赤白でかわいらしい。さらに基盤にリボンケーブルをつなぐ端子のよこに、CAMERAって書いてあったところから調べると、専用のカメラパーツがあったのでそれも。

Intel Edison

そして、なんか不完全燃焼な気持ちを鎮めるべく、Intel Edison with Breakout board Kitを新たにポチりました。金曜に届くそうなので次の週末に。ライトユーザーとしては選択余地がPI3の他になさそうなのですが、IoTというかWiFi+GPIO+Golangってことのみ要件としたときに、Raspberry PI3はHDMIやノーマルサイズのUSBx4やオーディオ出力に有線LANとそのまま一人前のPCとして使えるスペックとなってることが逆にロマンを欠いた気がしてます。はじめは長女きっかけでしたが、これはこれで教養としてひととおり修めてみようと調べてみれば、やっぱりブランドモノでIntelやっとくかな、最初に選ばなかったけどEdison触ってみたいなあ、そんな程度のことです。Edisonでは電池駆動を目指したい。

Raspberry PIを箱から出してGoアプリを動かす

Hello Raspberry!

8th gradeの長女(Mayaa)が最近いきなり電子工作に興味をもってブレッドボードなどを買ってきたので、対抗してRaspberry PIをはじめました。幸いにまだ臭いとかは言わないまでも、いずれ生ゴミ扱い仕掛けて来かねない思春期の娘に対して、父が一度はドヤ顔をする予定。

Raspberry PIを購入し、家にあるものかき集め

  • Raspberry PI 3 model B、今回唯一の購入品。Amazon.comで新品$36.91
  • micro SSD 4GBとSSDジャケット
  • iPad用アダプタ 12wとmini USBケーブル。2.4Aなので推奨2.5A以上というのを満たさないが問題ない
  • ルーターとLANケーブル、Comcastの引込み線が居間にあるうえ、Google OnHubなので有線ポートが一つしかないから設置条件が著しく制限されてるけど、一旦WiFi設定したら抜いて自室へ移動させるので問題ない
  • MacBook Pro。一世代前のものなのでSSDが直接挿せる
  • iPhone 5s。普段使いのスマホで、今回はOnHubを見るためのもの。

OS書き込み、起動

f:id:masataka_k:20161112111533p:plain

まずとにかくOSをダウンロードしてきて、SSDに書き込む。SSDが4GBしかないし、キーボードもモニターも余ってないのでヘッドレスで全部やることとして、GUIは諦めてRaspbianのJessie Liteの方にしました。MBPにジャケットを着せたmicro SSDを挿してOSインストールします。SSDはすでにFAT32でフォーマットしてあった。

Installing operating system images on Mac OS - Raspberry Pi Documentation

$ sudo diskutil unmountDisk /dev/disk2
$ sudo dd bs=1m if=~/2016-09-23-raspbian-jessie-lite.img of=/dev/rdisk2

SSDを抜き、Raspberry PI本体に挿す。LANケーブルとマイクロUSB電源ケーブルを接続すると、起動。

f:id:masataka_k:20161112112327j:plain

写真はインスタグラムにアップするために撮ったんだけど、実はこのときは書き込み失敗してたみたいでちゃんと上がってませんでした。書き込み成功すると勝手にマウントされるのでわかります。正常動作中は通電の赤LEDが常時点灯+動作状態表示の緑LEDが点滅します。

間違ってLinux PCでの作業ドキュメント読んでてddコマンドのパラメータでbs=4mとあったので1回目は間違ってそうしてましたがそれがいけなかったのか、それともフォーマットが悪かったのか。フォーマットしなおした後でbs=1mでの書き込みで成功しています。

SSHでアクセス

SSH using Linux or Mac OS - Raspberry Pi Documentation

iPhoneのOnHubアプリでIPを調べる。"raspberrypi"で見つかった。今回は192.168.86.107。

$ ssh pi@192.168.86.107
The authenticity of host '192.168.86.107 (192.168.86.107)' can't be established.
ECDSA key fingerprint is SHA256:C3eTyJeoxwm5/+PgxlEwFy7bQYUSHn4v3beRUGiWg8E.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '192.168.86.107' (ECDSA) to the list of known hosts.
pi@192.168.86.107's password: 

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.

WiFiを起こす

Setting WiFi up via the command line - Raspberry Pi Documentation

pi@raspberrypi:~ $ sudo vi /etc/wpa_supplicant/wpa_supplicant.conf

あらかじめ $ sudo wpa_passphrase P807NETWORK password でつくったnetwork設定ブレースを末尾にコピペ。P807NETWORKはウチのSSIDで、米国にくる前に住んでたマンションの部屋番号由来です。pskはところどころxでつぶしてます。

デフォルトではcountryがGBだったのでUSに変更。Raspberry PIはたしか英国発でしたね。

# /etc/wpa_supplicant/wpa_supplicant.conf
country=US
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
    ssid="P807NETWORK"
    psk=70a7fc3xx0aa1f72f8xfa58xxcd7aa833xxx6023e4xx1d621119cb9d3e20bd81
}

反映。

pi@raspberrypi:~ $ sudo ifdown wlan0
ifdown: interface wlan0 not configured
pi@raspberrypi:~ $ sudo ifup wlan0

LANケーブルはもう抜いちゃう。

確認。

OnHubアプリでもう一回IPを探すと、同じ"raspberrypi"で今回は192.168.86.113にみつかりました。sshで再接続。

pi@raspberrypi:~ $ ifconfig
eth0      Link encap:Ethernet  HWaddr b8:27:eb:44:d1:0b  
          UP BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:149 errors:0 dropped:2 overruns:0 frame:0
          TX packets:39 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:25148 (24.5 KiB)  TX bytes:6210 (6.0 KiB)

lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

wlan0     Link encap:Ethernet  HWaddr b8:27:eb:11:84:5e  
          inet addr:192.168.86.113  Bcast:192.168.86.255  Mask:255.255.255.0
          inet6 addr: fe80::e987:c595:7e09:9dae/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:2449 errors:0 dropped:25 overruns:0 frame:0
          TX packets:175 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:387234 (378.1 KiB)  TX bytes:24169 (23.6 KiB)

ストレージのほうも確認しておきます。。。が、何をどう見ればいいんでしょうか。。。うーんと、fdisk。

pi@raspberrypi:~ $ sudo fdisk -l
Device         Boot  Start     End Sectors  Size Id Type
/dev/mmcblk0p1        8192  137215  129024   63M  c W95 FAT32 (LBA)
/dev/mmcblk0p2      137216 7774207 7636992  3.7G 83 Linux

4GBのSDカードだから、おそらくいじる必要ないんじゃないかな。

netatalkでファイル共有する

ファイルをリモートで共有することで、ssh上で書き物するめんどくささを解消する。

pi@raspberrypi:~ $ sudo apt-get update
pi@raspberrypi:~ $ sudo apt-get install netatalk

あとはFinderで"移動" > "サーバへ接続..."のダイアログで、afp://192.168.86.113へ接続する。

f:id:masataka_k:20161112111440p:plain

時刻設定

/etc/ntp.conf を書き換え。http://www.pool.ntp.org/zone/us より、以下を書けといわれた。

# /etc/ntp.conf

(省略)

server 0.us.pool.ntp.org
server 1.us.pool.ntp.org
server 2.us.pool.ntp.org
server 3.us.pool.ntp.org

設定書き換えた後、ntpをリスタートする。

pi@raspberrypi:~ $ sudo service ntp restart

Go Raspberry!

まずは、Go SDKのディストロをダウンロードしてきて、MBPとRaspberry PIのファイル共有で転送します。ファイル大きくないし1個だからすぐ。

Downloads - The Go Programming Language

ファイルはarmv6lのフィックスが入ったファイル名のもの。最新1.7.3でもちゃんとありました。

pi@raspberrypi:~ $ sudo tar -C /usr/local/ -xzf ~/go1.7.3.linux-armv6l.tar.gz

展開はちょっと時間かかる。インストールは以上。

pi@raspberrypi:~ $ mkdir go
pi@raspberrypi:~ $ mkdir go/vendor
pi@raspberrypi:~ $ vi .bash_profile

Go関係の環境変数を設定します。

# .bash_profile
export PATH=/usr/local/go/bin:$PATH
export GOPATH=~/go/vendor:~/go

GOPATHはVendoring対応しときました。おもむろにgo getしてみようとしたら、gitがないって怒られた。

pi@raspberrypi:~ $ source .bash_profile
pi@raspberrypi:~ $ go version
go version go1.7.3 linux/arm
pi@raspberrypi:~ $ sudo apt-get install git
pi@raspberrypi:~ $ go get goji.io

予定通りきれいにgoji.ioのソース群がvendorフォルダの下に入ってくれました。

helloサーバー起動

Goji

goji.ioのトップにあるサンプルをそのままコピペしてgo runして。。。ブラウザから192.168.86.113:8000が見えない。接続が拒否された。。。sshで別に接続してローカルにcurlすると動く。。。あ、localhostの名前解決の問題か!

// ~/go/src/hello/hello.go
package main
import (
        "fmt"
        "net/http"
        "goji.io"
        "goji.io/pat"
)

func hello(w http.ResponseWriter, r *http.Request) {
        name := pat.Param(r, "name")
        fmt.Fprintf(w, "Hello, %s!", name)
}

func main() {
        mux := goji.NewMux()
        mux.HandleFunc(pat.Get("/hello/:name"), hello)
        http.ListenAndServe(":8000", mux)
}

上記のようにGoソースを書き換えて、ビルド。

pi@raspberrypi:~ $ go build hello
pi@raspberrypi:~ $ ./hello&
[1] 15120

最初のgoji.ioサンプルはhttp.ListenAndServeの引数で"localhost:8000"と指定しているからダメでした。上記のように":8000"とポート番号だけにしておけばOK。ブラウザからhttp://192.168.86.113:8000/hello/masatakaをたたくと、ちゃんとでてきます。

GPIO, PMW

GPIO: Models A+, B+, Raspberry Pi 2 B and Raspberry Pi 3 B - Raspberry Pi Documentation

Raspberry Pi with Gobot

GitHub - sarfata/pi-blaster: PWM on the Raspberry pi - done properly (in hardware, stable)

ブレッドボードとつなぐのはGPIOというもの利用するんですね。汎用目的入出力。ブレッドボードのキットの方に繫ぐための線がついていました。ドキュメントにLED光らせるのがあるので、始めるならここからかな。そして抽象化したライブラリのGobotと、GobotのインフラとしてのPI-Blasterでコントロールする。。。

pi@raspberrypi:~ $ sudo apt-get install autoconf
pi@raspberrypi:~ $ git clone https://github.com/sarfata/pi-blaster.git
pi@raspberrypi:~ $ cd pi-blaster
pi@raspberrypi:~/pi-blaster$ ./autogen.sh 
pi@raspberrypi:~/pi-blaster$ ./configure 
pi@raspberrypi:~/pi-blaster$ make
pi@raspberrypi:~/pi-blaster$ sudo make install

PI-Blasterのドキュメントにあるとおりに$ sudo apt-get install pi-blaster でインストールしてみようとしたら、一時的なのかわかりませんが"E: Unable to locate package pi-blaster"とエラー出しましたので、githubからクローンしてRaspberry上でmakeする方法としています。ドキュメントのExampleから、PI-Blasterは標準入力でPINに命令を送る仕組みと理解しました。そんなこんなで知らない単語として「PWM」というのが出てきたので調べると。。。

PWM(Pulse Width Modulation)とは、半導体を使った電力を制御する方式の1つです。オンとオフの繰り返しスイッチングを行い、出力される電力を制御します。

引用:PWMとは | 東芝 ストレージ&デバイスソリューション社

。。。気をとりなおして、Gobotをgo get。

pi@raspberrypi:~/pi-blaster$ cd ~
pi@raspberrypi:~$ go get -d -u github.com/hybridgroup/gobot/...
pi@raspberrypi:~$ go install github.com/hybridgroup/gobot/platforms/raspi

LEDチカチカ

// ~/go/src/blinker/blinker.go
package main
import (
        "time"
        "github.com/hybridgroup/gobot"
        "github.com/hybridgroup/gobot/platforms/gpio"
        "github.com/hybridgroup/gobot/platforms/raspi"
)

func main() {
        gbot := gobot.NewGobot()
        r := raspi.NewRaspiAdaptor("raspi")
        led := gpio.NewLedDriver(r, "led", "7")
        work := func() {
                gobot.Every(1*time.Second, func() {
                        led.Toggle()
                })
        }
        robot := gobot.NewRobot("blinkBot",
                []gobot.Connection{r},
                []gobot.Device{led},
                work,
        )
        gbot.AddRobot(robot)
        gbot.Start()
}

こちらのGobotページにあったサンプルをそのまま流し込みました。そしてビルド&実行。

pi@raspberrypi:~$ mkdir ~/go/src/blinker
pi@raspberrypi:~$ vi ~/go/src/blinker/blinker.go
pi@raspberrypi:~$ go build blinker
pi@raspberrypi:~$ ./blinker 
2016/11/12 01:41:39 Initializing Robot blinkBot ...
2016/11/12 01:41:39 Initializing connections...
2016/11/12 01:41:39 Initializing connection raspi ...
2016/11/12 01:41:39 Initializing devices...
2016/11/12 01:41:39 Initializing device led ...
2016/11/12 01:41:39 Starting Robot blinkBot ...
2016/11/12 01:41:39 Starting connections...
2016/11/12 01:41:39 Starting connection raspi...
2016/11/12 01:41:39 Starting devices...
2016/11/12 01:41:39 Starting device led on pin 7...
2016/11/12 01:41:39 Starting work...
^C2016/11/12 01:42:27 Stopping Robot blinkBot ...

Crrl+Cで止めました。きちんと動いているっぽいけど、LEDを接続まだしていないので見かけ変わらない。。。ここで長女登場でブレッドボードを組んでもらいました。

f:id:masataka_k:20161113192539j:plain

サンプルコードで、NewLedDriverの引数に渡されている「7」が物理pin番号の7。GPIOの7番ではない。この7番pinにプラス、39番pin(一番外側のground)にマイナスで、組んだ回路につなげて実行したら、プログラムどおりに1秒毎にトグルされてブレッドボードに刺したLEDがゆっくりとチカチカしました!

このとき注意ポイントは実行時にはsudoで実行すること。最初は知らずにしばらく沈黙してました。GPIO操作はルート権限。まあ、そうでしょうね。

電源オフ

pi@raspberrypi:~ $ sudo shutdown -h now

おー、久しぶりにこのコマンド叩いたよ!クラウド時代には絶対唱えないshutdown。しばらく基盤の緑LEDがチカチカしたのちに基盤の赤LEDのみ点灯となります。電源ケーブル抜いて、おしまい。

感想

想像以上に整備されちゃってて普通のLinux&普通のGoの世界まで速攻で来れちゃいました。今回程度のことなら容量もパワーも十分にあったのでRaspberry PIの上にSDKいれてgo getからアプリケーションビルドまでしましたが、クロスコンパイルする方が筋がよいかもしれません。

モニター&キーボードをつながないなら、Intel Edisonのほうが攻めてる感じになったかなと考えましたが、Raspberry PIのほうが価格が安く、それゆえかケースなどの周辺も充実してるんでOK。