6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Raspberry piから送信したセンサーの値をGoのWEBサーバーで受け止め、three.jsで立体的に連続表示させる

Posted at

背景

前回の下記記事の続きで、センサーの値をGoで立てたWEBサーバーにUPし、ブラウザ上にthree.jsで表示させるお話。前回同様、とりあえずは動くものを目指します。

RaspberryPi 3B+で感圧センサーの値を、A/Dコンバーターを経由してI2CでNode.jsを使って取得する

完成イメージ

動くとこんな感じです。

動作イメージ

動作イメージ

構成図

構成図

GoのWeb Application FrameworkのGin

今回はGoのWeb Application Framework(WAF1)のGinを使ってみます。Go自体かなりシンプルかつ強力な言語なので、WAFがいらないほどのようなのですが、フレームワークを使うことで色々と面倒な部分が減るのと、Go言語のWAF事情2についても知りたかったので導入することにしました。

フォルダ構成

どこまで整理するか悩んだのですが、今回はそこまで行数も多くないので、main.goとindex.htmlに全て記載することにしました。ただ、内部では一応関数を分けて、雰囲気MVCぽくなるようにしました。

Server
│  main.go
│  sensor.json
│
└─views
        index.html

main関数でルーティングを決める

さて、まずはmain.goのmain関数で下記のようにルーティングを決めていきます。一旦役割分担として、main関数にはルーティングを担わせます。

main.go
package main

const (
	//Port サーバーのポート番号を指定
	Port string = "80"
)

//main では基本的にURL管理を行う
func main() {
	router := gin.Default()

	router.LoadHTMLGlob("views/*.html")  //Viewsフォルダにテンプレートがあることを指定

	router.GET("/", Index)
	router.GET("/pres-sensor/current", GetPresSensorCurrent)
	router.POST("/pres-sensor/current", PostPresSensorCurrent)

	router.NoRoute(NoRoute) //ルーティングが指定されていない時の処理

	router.Run(":" + Port)
}

センサーの値をstructで定義

次にセンサーの値をstructとして定義します。structとして定義しておくことで後々JSONとして扱うのが楽になったり、ちゃんと型チェックを行うことでバグを予防することができます。ついでにjsonとして扱う際のキーもコメントで記載しておきます。

main.go
//Sensor センサー
type Sensor struct {
	Name  string  `json:"name"`
	Volts float64 `json:"volts"`
	Value int     `json:"value"`
}

センサーの情報を返す

先ほど定義したセンサー情報をJSONで返したり、JSONで受け取ったりする関数を描いていきます。シンプルな作りなので必要ないと言えばないのですが、ModelっぽくデータI/Oを関数で切り出しておきます。

main.go

//GetPresSensorCurrent 現在の感圧センサー情報の取得
func GetPresSensorCurrent(ctx *gin.Context) {
	sensor := readPresSensorCurrent()
	ctx.JSON(http.StatusOK, sensor)
}

//PostPresSensorCurrent センサー情報をバインド
func PostPresSensorCurrent(ctx *gin.Context) {
	var sensor Sensor
	if err := ctx.Bind(&sensor); err != nil {
		log.Fatal(err)
	}
	writePresSensorCurrent(sensor)
	ctx.JSON(http.StatusOK, sensor)
}

Webサービス上でのセンサー情報の保存

センサーの情報はいったん一時的にJSONファイルへ書き出して保存します。今回は取り急ぎぶら下げるRaspberry piが一つだけで、見るのも自分だけなので、とりあえずの実装としてJSONファイルでの保存を行います。

main.go
const (
	//JSONFileName センサー情報を扱うjsonファイル名を指定
	JSONFileName string = "sensor.json"
)

//readPresSensorCurrent jsonファイルからセンサー情報を取得する
func readPresSensorCurrent() Sensor {
	bytes, err := ioutil.ReadFile(JSONFileName)
	if err != nil {
		log.Fatal(err)
	}
	var sensor Sensor

	if err := json.Unmarshal(bytes, &sensor); err != nil {
		log.Fatal(err)
	}
	return sensor
}

//writePresSensorCurrent jsonファイルにセンサー情報を書き出す
func writePresSensorCurrent(sensor Sensor) {
	sensorJSON, err := json.MarshalIndent(sensor, "", "  ")
	if err != nil {
		log.Fatal(err)
	}

	if err := ioutil.WriteFile(JSONFileName, sensorJSON, 0666); err != nil {
		log.Fatal(err)
	}
}

実際にJSONファイルに書き出すと下記のようなファイルが生成されます。まだセンサー情報をサーバーに上げる部分を作っていないので、試しにテキストエディタで作成しておくと後の表示部分を組みやすいです。

sensor.jsonのサンプル
{
  "name": "1",
  "volts": 0.138095419944593,
  "value": 1700
}

ファイルI/Oの競合問題とデータベース

この方法だとトランザクション管理がされていないので、同時に読み書きしてしまうとデータが破綻してしまうのであまりよろしくはないです。本来であればトランザクション管理が行えるMySQLやPostgreSQLなどのDBエンジンを使用すべきです。ただ、今回の目的は表示させるまでの一連の流れを追いたいだけなので、一旦お手軽JSONファイルで実装し、後でDBを導入することにします。

ここまでで、WEBサーバー上での処理はほとんど終わりで、後はブラウザ上で表示させるView周りを作り、センサー情報をIoTから送ってあげるようにすれば、一連の流れが出来上がります。

ブラウザ上でVue.jsとAxiosで値を表示させる

WEBサーバー上の処理が大まかに終わったので、ブラウザ上での挙動を進めていきます。今回はまずAxiosでサーバー上の値を取ってきて、Vue.jsとでセンサーの値を画面上に表示させます。

基本構成のindex.htmlを作る

最初に、基本的なHTMLの部分を作成していきます。viewsフォルダの中にindex.htmlというファイルを作成し、下記のようにします。今回はunpkgのCDNを使います3

index.html
<!doctype html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>測っちゃう君</title>
</head>
<body>
<div id="pres-sensor">
  Value: ${Value}<br>
  Volts: ${Volts}
</div>
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<!-- ここに処理を記載していきます -->
</body>
</html>

index.htmlをブラウザ上で確認する

試しにindex.htmlをブラウザ上で確認してみましょう。「go run main.go」をコンソール上で叩くとGoによるWEBサーバーが立ち上がります。ブラウザ上で「http://localhost/」にアクセスしてみると下記のように表示されるのが確認できます。

Value: ${Value}
Volts: ${Volts}

必要なくなればCtrl+Cなどで停止します。
Go周りの修正については、確認するために毎回go runをし直す必要がありますが、これから書き換えていくindex.htmlなどのテンプレートファイルやJSONファイルは毎回LoadHTMLGlobで読み込んでいるので、go runしっぱなしでも大丈夫です。ブラウザ上で再読み込みすれば変更が反映されます。

Vue.jsでセンサーの値を書き換える

まずはVue.jsでセンサーの値を書き換えられるようにVue.jsで指定しましょう。まだデータは取ってこれないので、一旦「nodata」と表示させられるようにします。

index.html
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
const vm = new Vue({
  el: '#pres-sensor',
  delimiters: ['${', '}'],
  data: {
      Value: "nodata",
      Volts: "nodata"
    },
});
</script>

Axiosでセンサーの値を取ってきてVue.jsで定義したセンサーの値を上書きする

次にサーバー上の値を取ってきます。下記のようにaxiosを使ってセンサー情報をサーバーから取ってくる関数を作成します。

index.html
<script>
//GetPresSensorCurrent サーバー上から現在のセンサー情報を取得する。
function GetPresSensorCurrent(){
  axios.get('/pres-sensor/current').then( response => {
  vm.Value = response.data.value;
  vm.Volts = response.data.volts;
  })
}
</script>

ついでに、ページの読み込み後にセンサー情報を取ってこれるようにしてみましょう。

index.html
<script>
// ページの読み込みを待つ
window.addEventListener('load', init);

//初期化
function init() {
  GetPresSensorCurrent();
}
</script>

これで、ページを読み込んだ直後にサーバー上のJSONファイルから取ってきた情報がブラウザ上で確認出来るようになりました。おそらく、最終的に下記のように表示されるハズです。

Value: 1700
Volts: 0.138095419944593

three.jsで棒グラフを立体的に表示する

さて、センサー情報を取ってくるところまでは成功したのですが、どうせなら連続値を扱いたいと思います。今回は見るだけなので、サーバー上でデータをキャッシュせず、まずはブラウザ上でキャッシュさせながら表示することにします。
連続値を見ると言えば折れ線グラフなのですが、three.jsを使いたい欲が出たので今回はthree.jsで実装を進めていきます。

canvasを設置して棒グラフを表示させる

まずはセンサー情報を表示していた部分にcanvasを追記しておきます。

index.html
<div id="pres-sensor">
  Value: ${Value}<br>
  Volts: ${Volts}
</div>
<canvas id="sensor-graph"></canvas>

さらに下記のように棒グラフを表示させる土台となるレンダラー、シーン、カメラを準備しておき、棒グラフを配置します。

index.html
<script src="//unpkg.com/vue/dist/vue.min.js"></script>
<script src="//unpkg.com/axios/dist/axios.min.js"></script>
<script src="//unpkg.com/three/build/three.min.js"></script>
<script src="//unpkg.com/three/examples/js/controls/OrbitControls.js"></script>

<script>
const vm = new Vue({
  el: '#pres-sensor',
  delimiters: ['${', '}'],
  data: {
      Value: "nodata",
      Volts: "nodata"
    },
});

//canvasのサイズ指定
const width = 500;
const height = 300;

// 棒グラフのサイズとMaterialを設定
const geometry = new THREE.BoxGeometry(40, 40, 40);
const material = new THREE.MeshNormalMaterial();

// 感圧センサーのバーを集めたグループを用意しておく
const presBarGroup = new THREE.Group();


// ページの読み込みを待って初期化
window.addEventListener('load', init);
function init() {

  // レンダラーを作成
  const renderer = new THREE.WebGLRenderer({
    canvas: document.querySelector('#sensor-graph')
  });
  renderer.setPixelRatio(window.devicePixelRatio);  //解像度を合わせる
  renderer.setSize(width, height);  //canvasサイズ指定

  // シーンを作成
  const scene = new THREE.Scene();

  // カメラを作成
  const camera = new THREE.PerspectiveCamera(45, width / height);
  camera.position.set(+1000, +500, +500); //カメラ位置指定
  const controls = new THREE.OrbitControls(camera); // カメラコントローラーを作成
  controls.enableDamping = true;  // カメラコントローラーの制御を滑らかにする(ダンプをつける)
  controls.dampingFactor = 0.2;

  //棒グラフグループを作成して配置
  presBarGroup.position.x = width;
  scene.add(presBarGroup);

  //毎フレームの動作を実行。
  tick();

  //サーバーからセンサー情報を定期的に取ってくる
  var timer = null;
  timer = setInterval(GetPresSensorCurrent, 1000);

  function tick() {
    renderer.render(scene, camera); // レンダリング
    requestAnimationFrame(tick);
  }
}
//GetPresSensorCurrent サーバー上から現在のセンサー情報を取得する。
function GetPresSensorCurrent(){
  axios.get('/pres-sensor/current').then( response => {
    vm.Value = response.data.value;
    vm.Volts = response.data.volts;

    //棒グラフを新規で作成
    const presBar = new THREE.Mesh(geometry, material);
    presBarGroup.add(presBar);  //棒グラフグループに新しい棒グラフを加える
  })
}
</script>

ここまでで、下記のような感じになるハズです。Volts、Valueの値は先ほど作成したJSONファイルの中身が表示されています。棒グラフは基本形の立方体が表示されています。

image.png

Axiosでセンサーの値を取ってきて棒グラフに反映させる

さて、次にサーバー上のデータを取ってきて棒グラフに反映させましょう。せっかくなので連続して取ってきた値が表示されるようにしたいと思います。実は既にサーバー上の値をAxiosで取るたびに棒グラフが生成されるところまでは組み終わっています4。後はこれを徐々に動かしていけばOKです。
イメージとしては、canvasにシーンを配置し、その中に棒グラフグループ(presBarGroup)を配置して、グループごとじわじわと遠方へ動かしていきます。ベルトコンベアのように移動する棒グラフグループの上に、一つずつ現在のセンサー情報を乗せた棒グラフを置いていくイメージです。
さらに、棒グラフの高さをサーバー上から取ってきたValue値にします。ちょっと値が大きいので、500で割って四捨五入しておきます。

index.html
<script>
//GetPresSensorCurrent サーバー上から現在のセンサー情報を取得する。
function GetPresSensorCurrent(){
  axios.get('/pres-sensor/current').then( response => {
    vm.Value = response.data.value;
    vm.Volts = response.data.volts;

    //棒グラフを新規で作成
    const presBar = new THREE.Mesh(geometry, material);
    presBarGroup.position.x -= 40;  //既存の棒グラフグループの位置をずらして
    presBar.position.set(presBarGroup.position.x * -1 + width,0,0); //棒グラフの位置を合わせる
    presBar.scale.y = Math.round(response.data.value / 500);  //値を棒グラフの高さに設定
    presBarGroup.add(presBar);  //棒グラフグループに新しい棒グラフを加える
  })
}
</script>

ここまでで、下記のようになっているハズです。これでサーバー上の値を連続して取ってくることが出来るようになりました。

graph.gif

Raspberry piからセンサー情報をWEBサーバーに送る

前回の記事で作成したモジュールを使って、センサー情報をサーバーに送ります。

RaspberryPi 3B+で感圧センサーの値を、A/Dコンバーターを経由してI2CでNode.jsを使って取得する

Node.jsでrequestモジュールを設定する

前回と同様に下記のサンプルを基に作成していきます。前回と違って、今回はサーバーの在処であるIPアドレスもしくはURLを指定し、そこに取得したセンサー情報をPOSTしてあげる必要があります。

ADS1x15 Interface over I2C

まずは下記のように初期設定を行います。requestモジュールを使ってPOSTをしていくのですが、その初期設定として通信先のサーバー、ヘッダー情報とセンサー情報が取れなかった時の初期情報を入れておきます。

index.js
"use strict";

const Raspi = require('raspi');
const I2C = require('raspi-i2c').I2C;
const ADS1x15 = require('raspi-kit-ads1x15');

//Webサーバーのホスト名
const serverHost = "192.168.1.250";

//WEBサーバーへの通信初期設定
var request = require('request');
var options = {
  uri: "http://" + serverHost + "/pres-sensor/current",
  headers: {
    "Content-type": "application/x-www-form-urlencoded",
  },
  form: {
    "Name": "NoData",
    "Value": "NoData",
    "Volts": "NoData"
  }
};

センサー情報を読み取りPOSTする

センサー情報を読み取る部分をreadSensor関数として切り出し、そこを定期的に呼び出すようにします。前回はセンサー情報を呼び出したらすぐにprocess.exitしていましたが、今回は定期実行したいのでこれをコメントアウトしておきます。細かいところですが、consoleに出力するログも1行にして、連続したデータを見やすくしておくと良いです。

index.js
Raspi.init(() => {
  const i2c = new I2C();
  const adc = new ADS1x15({
      i2c,
      chip: ADS1x15.chips.IC_ADS1115,
      address: ADS1x15.address.ADDRESS_0x48,

      // Defaults for future readings
      pga: ADS1x15.pga.PGA_4_096V,            // power-gain-amplifier range
      sps: ADS1x15.spsADS1115.SPS_250         // data rate (samples per second)
  });

  //センサー情報を読み込むタイマーをセットし、無限ループさせる。
  var timer
  timer = setInterval(readSensor,1000);

  console.log('server host: ', options.uri);

  //センサー情報を読み込む
  function readSensor(){
    adc.readChannel(ADS1x15.channel.CHANNEL_0, (err, value, volts) => {
      if (err) {
        console.error('Failed to fetch value from ADC', err);
        process.exit(1);
      } else {
        console.log('Channel 0 * Value:', value, ' * Volts:', volts);
        options.form.Name = "0";
        options.form.Value = value;
        options.form.Volts = volts;
        request.post(options, function(error, response, body){});
        //process.exit(0);
      }
    });
  }
});

完成

完成するとこんな感じになります。

動作イメージ

ちなみに、こっそり下記のコードでOrbitControlsを仕込んでおいたので、マウスでこんな感じに動かして遊ぶこともできます。

index.html
<script src="//unpkg.com/three/examples/js/controls/OrbitControls.js"></script>
index.html
const controls = new THREE.OrbitControls(camera); // カメラコントローラーを作成
controls.enableDamping = true;  // カメラコントローラーの制御を滑らかにする(ダンプをつける)

カメラ移動

流石three.js、ちょっと動かしただけなのにかっこいい!

  1. セキュリティやってる身からするとWAFと見た時にWeb Application Firewallを思い浮かべてしまうのですが、Go界隈でWAFというと大抵Web Application Frameworkを指すようですね。

  2. Ruby on RailsやPHP系のWAFだと結構フォルダ構成含めて縛りが多いのですが、Goだとその辺りの自由度が高く、最初は戸惑いました。またIris周りの騒動5とかも他ではあまり聞かなかったので地味に注意点あるなぁと。Irisはサンプルとかも充実していて良さそうなのですが、うぅむ。

  3. three.jsのOrbitControls.jsをCDNから使いたかったのでunpkgを使用しました。本番実装する時はリスク回避のためアプリの中にjsファイルも組み込んだ方が良いと思います。

  4. わざわざGetPresSensorCurrent関数にnew THREE.Meshを入れているのがそれです。GetPresSensorCurrent関数を呼ぶ度に棒グラフの新しい要素が出現しています。

  5. http://www.florinpatan.ro/2016/10/why-you-should-not-use-iris-for-your-go.html

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?