ラズパイ3を使ったIoTシステム構築

#概要
ラズパイ3とさくらインターネットのレンタルサーバ(※)を使って、自宅IoTシステムのベースを構築したので備忘録的を兼ねて整理。
　※レンタルサーバライト，年額約1500円

やりたいことは以下の通り。

1. ラズパイ3からデータを定期的にサーバにアップロードする。（今回はラズパイのCPU温度を取得してアップロード）
2. サーバ側で受信したデータをDBに入れる。（契約していたレンタルサーバの制約でSQLiteを使用）
3. PC，スマホからグラフ表示用URLへのアクセスで、DBからデータを引っ張る。
4. 3で引っ張ったデータで時系列グラフを描画する。（グラフ描画はChart.jsを使用）

全体的な構成は以下の通り。
![全体概要.png](https://qiita-image-store.s3.amazonaws.com/0/177972/9641eebf-75f6-6bf3-ba1f-c1f8aa5161e1.png)

今回はシステムのベースを構築するのが目的のため、あまり面白いことはやりません。改造はまた別の機会にします。

（1から記事を書くと構築時のデバッグや単体テストがしづらいと思うので）記事としては3→4→2→1の順番で整理します。

# 目次
1. DBからデータを引っ張る
2. Chart.jsでグラフ描画
3. DBにデータを入れる
4. ラズパイからデータ送信


## 1. DBからデータを引っ張る
DBのテーブル想定は以下の通り。
DBファイル名：temp_pi.db
DBテーブル名：temp_pi
テーブル内カラム一覧
　id：データ登録毎に1ずつ増えていく整数，主キー
　temp：CPU温度
　created_datetime：データ登録時の時間

デバッグ時は参考サイトで挙げるSQLiteSpy(DB閲覧編集ソフト)などのツールでDBファイルを作成してやれば良い。

```php:request_data.php
<?php
switch ($_SERVER['REQUEST_METHOD']) {
	case 'GET':
		// DBへ接続
		try {
		  $db = new SQLite3('./temp_pi.db');
		} catch (Exception $e) {
		  print 'DBへの接続でエラーが発生しました。<br>';
		  print $e->getTraceAsString();
		  break;
		}

		// データの取得
		$sql = 'SELECT temp,created_datetime FROM temp_pi ORDER BY id DESC LIMIT 100';
		$res = $db->query($sql);
		
		// 結果格納用配列の宣言
		$result = array(); 
		
		// 結果を配列に格納
		while( $row = $res->fetchArray(SQLITE3_ASSOC) ) {
			$result[]=array(
				'time'=>$row['created_datetime'],
				'y1'=>(float)$row['temp']
			);
		}
		
		// JSON形式で出力
		header('Content-type: application/json');
		echo json_encode($result);
}
?>
```

上記ソースでのポイントは以下の通り。

- SQLiteはDBをファイルで作成するので、DBファイルへのアクセスとなる。
- DBアクセスは念のため、DB接続時のエラー処理を入れている。
- データ取得は最新100件を上限にして取得している。最新は「ORDER BY id DESC」、100件は「LIMIT 100」の部分に対応。
- 取得データを配列(result[])に格納する処理では、SQLITE3_ASSOC(返された結果セットのカラム名をインデックスとする配列を返す)オプション指定で実行。
- 配列(result[])に格納したデータをJSON形式でクライアントに返す。「header('Content-type: application/json');」でJSON形式のヘッダー指定を行い、「echo json_encode($result);」でデータを出力する。他に余計なデバッグ用printがあるとクライアントがJSON形式のデータを認識できないので注意。

＜本項での参考サイト一覧＞
- [SQLite3のドキュメント](http://php.net/manual/ja/book.sqlite3.php)
- [PHPでのSQLite有効化](https://www.phpbook.jp/tutorial/sqlite/index1.html)
- [SQLite3でデータ取得参考](https://gray-code.com/php/select-by-using-sqlite3/)
- [SQLite3コマンドサンプル](http://iatlex.com/linux/first_sqlite/)
- [SQLiteの閲覧編集(CLI)ツール](https://www.dbonline.jp/sqlite/)
- [SQLiteの閲覧編集(GUI)ツール](https://www.yunqa.de/delphi/products/sqlitespy/index)

## 2. Chart.jsでグラフ描画
g_temp_pi.htmlにアクセスすると、request_data.php経由でDBからデータを取得してグラフ描画する。

```g_temp_pi.html
<!doctype html>
<html>

<head>
	<title>Line Chart</title>
<!--	<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> -->
	<script src="jquery-3.3.1.min.js"></script>
	<script src="moment.js"></script>
	<script src="Chart.js"></script>
</head>

<body>
	<div style="width:95%;">
		<canvas id="canvas"></canvas>
	</div>
	<br>
	<br>
	

	<ul id="list"></ul>
	
	<p>Ajax成否：<span id="out1_1"></span>,<span id="out1_2"></span></p>
	<p>Ajax結果：<span id="out2"></span></p>
	<p>データ数：<span id="num"></span></p>

	<script type="text/javascript"> 

	// 配列の宣言，時間フォーマット指定
	var v_time = [];
	var v_y1 = [];
	var v_y2 = [];
	var timeFormat = 'YYYY-MM-DD HH:mm:ss';

	//画面構築完了後
	$(function() {       
		// 1.getJSONメソッドで通信を行う
		$.getJSON("http://hogehoge/request_data.php")
			
			// 2.doneは、通信に成功した時に実行される
			//  引数のdata1は、通信で取得したデータ
			.done(function(data1) {
				console.log("getJSON success!");
				
				// キーを指定して値を表示 (先頭データをdebug用として表示)
				$("#out1_1").html(data1[0]["time"]);
				$("#out1_2").html(data1[0]["y1"]);
				
				// 3.オブジェクトをJSON形式の文字列に変換
				var data2 = JSON.stringify(data1);
				console.log(data2); //コンソールにJSON形式で表示
				$("#num").html(data1.length); //データ長を表示
				console.log(data1.length);
				
				// 4.受け取ったデータは降順のため昇順に変更＆momentで時間軸データに変換
				for(var i=0; i<data1.length; i++){
					v_time[data1.length-i-1] = moment(data1[i]["time"], timeFormat);
					v_y1[data1.length-i-1] = data1[i]["y1"];
				}
				console.log(v_time);
				console.log(v_y1);
				
				// 5.画面構築とgetJSONでの処理が非同期のため、グラフ描画更新をかける。
				window.myLine.update();
			})
			
			// 6.failは通信に失敗した時に実行される
			.fail(function() {
				console.log("error");
				$("#out1").html("データ取得に失敗");
			})
			
			// 7.alwaysは成功/失敗に関わらず実行される
			.always(function() {
				console.log("complete");
				$("#out2").html("完了");
			});
		});
		
		//グラフ用の設定
		var config = {
			//グラフ種別設定　時系列グラフは'line'
			type: 'line',
			
			//データセットの登録
			data: {
				labels: v_time, //時間軸の配列
				//タイムスタンプは以下のような方法でゲットしておく
				// moment.unix( unixタイムスタンプ )
				// moment("2011/03/11 14:46:12", timeFormat)
				
				datasets: [{
					label: 'My First dataset',
					backgroundColor: Chart.helpers.color('rgb(255, 99, 132)').alpha(0.5).rgbString(),
					borderColor: 'rgb(255, 99, 132)',
					fill: false,
					data: v_y1, //系列1の数値配列
				}
//, {
//					label: 'My Second dataset',
//					backgroundColor: Chart.helpers.color('rgb(54, 162, 235)').alpha(0.5).rgbString(),
//					borderColor: 'rgb(54, 162, 235)',
//					fill: false,
//					data: value_b, //系列2の数値配列
//					data: v_y2, //系列2の数値配列
//				}
				]
			},
			
			//オプションの設定
			options: {
				title: {
					text: 'Chart.js Time Scale'
				},
				scales: {
					xAxes: [{
						type: 'time',
						time: {
							unit: 'minute',
							displayFormats: {minute:'HH:mm'}
						},
						scaleLabel: {
							display: true,
							labelString: 'Time[HH:mm]'
						}
					}],
					yAxes: [{
						scaleLabel: {
							display: true,
							labelString: 'value'
						}
					}]
				},
				elements: {
					line: {
						tension: 0, // ベジェ曲線を無効にする
					}
				},
			}
		};
		
		//画面構築が完了したらグラフ領域を作成
		window.onload = function() {
			var ctx = document.getElementById('canvas').getContext('2d');
			window.myLine = new Chart(ctx, config);
		};
	</script>
</body>
</html>
```
上記ソースでのポイントは以下の通り。

- ヘッダーでJavaScriptを読込む。CDNから読込む場合はコメントアウトしているようにする。今回はサイト内に設置したものを使用。jquery.jsはJSON形式でのデータ取得用、Chart.jsはグラフ描画用、moment.jsはChart.jsに対し横軸を時間軸にするための拡張用。
- getJSONメソッドでJSONデータを指定URLから取得。done以降ではデータ取得成功時の処理、fail以降ではデータ取得失敗時の処理、always以降ではgetJSONメソッドでの成功/失敗時に関わらず実行される処理になっているため適宜記述する。
- data1にJSON形式のデータが入っている。データは時間軸に対して降順のため、念のため昇順に変更すると共に、momemtメソッドでChart.jsが扱える時間軸形式に変更する。また、CPU温度データも取り出しておく。
- doneの最後で「window.myLine.update();」を入れないと画面の更新が上手くいかない。（入れない状態ではブラウザサイズ変更などで最新のデータが描画されるような動作になる。ブラウザのリロードでは描画エリアは出てくるが、データが表示されていない状態になる。）
- グラフ用の設定である「var config = {」以降の説明は見れば分かる範囲だと思うので省略。
- ベジェ曲線は無効にしておいた方が、時系列グラフとしては良い感じになると思う。

＜本項での参考サイト一覧＞
- [Chart.js公式](https://www.chartjs.org/)
- [Chart.js日本語ドキュメント](https://misc.0o0o.org/chartjs-doc-ja/)
- [Chart.jsを利用したCSVの時系列データのグラフ化](https://algorithm.joho.info/programming/javascript/chartjs-csv-data-graph/)
- [moment.jsの使い方](https://its-office.jp/blog/js/2016/04/02/moment.html)
- [スマホでのデバッグ方法](https://qiita.com/hojishi/items/12b726f8b02ef3d713e4)

## 3. DBにデータを入れる
ラズパイからのHTTPアクセスで、POSTメソッドで受け取ったデータをDBに入れる。

```html:regist_data.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

<title>データ登録</title>
</head>
<body>

<?php
	// Initialize
	$db = null;
	$sql = null;
	$res = null;

	$code = $_POST['code'];
	$v_0 = $_POST['v_0'];
	$v_1 = $_POST['v_1'];
	$v_2 = $_POST['v_2'];
	$v_3 = $_POST['v_3'];
	$v_4 = $_POST['v_4'];
	$v_5 = $_POST['v_5'];
	$v_6 = $_POST['v_6'];
	$v_7 = $_POST['v_7'];
	$v_8 = $_POST['v_8'];
	$v_9 = $_POST['v_9'];

	switch ($_SERVER['REQUEST_METHOD']) {
	case 'GET':
		print ('GETメソッドでアクセス<br>');
		break; //GETメソッドの終わり

	case 'POST':
		print ('POSTメソッドでアクセス<br>');
		print("code: $code <br>");

		if($code == "temp"){
			print ('code:tempでアクセス<br>');
			
			//DBへ接続
			try {
				$db = new SQLite3('./temp_pi.db');
			} catch (Exception $e) {
				print 'DBへの接続でエラーが発生しました。<br>';
				print $e->getTraceAsString();
				break;
			}
			
			// テーブルの存在確認
			$sql = 'SELECT count(*) FROM sqlite_master WHERE type="table" AND name="temp_pi"';
			
			// テーブルが存在しない場合は作成する
			if( !$db->querySingle($sql) ) {
				print('テーブルが存在しないため作成します<br>');
				
				// テーブルを追加
				$sql = "CREATE TABLE temp_pi(
					id INTEGER PRIMARY KEY,
					temp REAL NOT NULL,
					created_datetime TIMESTAMP DEFAULT (datetime(CURRENT_TIMESTAMP,'localtime'))
					)";
				$res = $db->exec($sql);
			}
			
			// データの追加
			$sql = "INSERT INTO temp_pi(temp) VALUES ($v_1)";
			$res = $db->query($sql);
			print("$sql <br>");
			
			//接続を終了
			$db->close();
			print('切断しました。<br>');
		}
		break; //POSTメソッドの終わり
}

?>
</body>
</html>
```

上記ソースでのポイントは以下の通り。

- POSTパラメータ「code」で、httpリクエストでの種別を場合分け。今回は"temp"の場合に、データをDBに入れる。
- SQLiteでのDB操作は「1.DBからデータを引っ張る」と基本的には同じ。
- DB用テーブルが存在しない場合(初回アクセスの場合)は、テーブルを作成する。
- 「INTEGER PRIMARY KEY」は、データ型がINTEGERのカラムに対してPRIMARY KEY制約を設定した場合、対象のカラムの値を省略するとAUTOINCREMENT制約を設定した時と同じように自動的に数値が格納される。
- 「created_datetime TIMESTAMP DEFAULT (datetime(CURRENT_TIMESTAMP,'localtime'))」は、対象のカラムの値を省略するとデータ登録時の日時が自動的に格納される。

<参考サイト>
- [デバッグ用httpリクエスト生成](https://hwada.hatenablog.com/entry/2013/10/28/123517)
- [INTEGER PRIMARY KEYの説明](https://www.dbonline.jp/sqlite/table/index7.html)
- 他のSQLite関連は「1.DBからデータを引っ張る」と同じ。

## 4. ラズパイからデータ送信
ラズパイからHTTPのPOSTメソッドでデータをサーバに送信する。今回はPythonを使用。

HTTPリクエスト用のPythonスクリプトは以下。第1引数でCPU温度(0.001℃単位での整数値)をもらうことが前提。

```python:requests_post.py
import sys
import requests

args = sys.argv
# args:cmd
# args[n]:argument[n]

# CPU Temperature [0.001deg]->[deg]
value = float(args[1]) / 1000

# Request to Server by Post Method
response = requests.post(
        'http://hogehoge/regist_data.php',
        {'code':'temp', 'v_1':str(value), 'v_2':'20'})
```

bashで実行するスクリプトは以下。catでCPU温度を取得し、パイプで前述のpythonスクリプトの第1引数に取得結果を渡している。

```sh:post_temp.sh
cat /sys/class/thermal/thermal_zone0/temp | xargs python /home/pi/requests_post.py
```

あとはpost_temp.shを定期的に実行させるようcrontabで設定すればよい。詳細は参考サイトを参照のこと。

<参考サイト>
[ラズパイ httpリクエスト](https://qiita.com/hththt/items/14bfc2bf23192b020371)
[crontab設定](https://qiita.com/hikouki/items/e744b3a4d356d2af12cf)
[パイプとxargs](http://bioinfo-dojo.net/2018/02/19/pipe_xargs/)
[pythonでの引数](https://www.python-izm.com/basic/command_line_arguments/)