IT女子のラズベリーパイ入門奮闘記

第22回「ラズベリーパイで手作り目覚まし時計!スヌーズ機能編」

raspberrypi22_main01

上の写真は、前回作ったレゴケースを土台に設置してみたところです。ラズベリーパイもブレッドボードも小さくて軽いため、作業中に配線が外れてしまったり、コードが絡まってしまったりすることが多かったので、レゴを使って簡易的に固定してみました。ブロックで囲うだけで、とっても使いやすくなりました!レゴ板、オススメです!
ちなみに上の写真はラズベリーパイA+を使ってWi-Fi環境での実験中の様子。負荷がかかりすぎるかな?と思ったのですが、フリーズしてしまうこともなく元気に稼働中です!

さてさて、4回にわたって連載してきたラズベリーパイアラームシリーズですが、今回はついに最終回!欲しい機能を追加して、最後の仕上げをしていきましょう!

今回は、長押しを使ったスヌーズ機能と、取得したデータを保存・閲覧できる機能を追加します。そして、個人的にちょっと気になっていた部分、音の停止方法を少しバージョンアップ。プロセスIDを使用して、IDごとに終了させる方法を試してみたのでご紹介します!

raspberrypi22_main2

 

長押しの判定方法

スヌーズ機能を実装する前に、スヌーズの停止処理について考えておきましょう。ボタンを2つ取り付けても良いのですが、どちらが何のボタンなのか分からなくなってしまいそうなので、今回は1つのボタンで進めます。

1つのボタンで2種類の命令を振り分ける操作といえば「長押し」ですね!
すぐに指を離すとアラーム停止、長押しするとスヌーズ停止というように、タクタイルスイッチ(タクトスイッチ)1つで制御してみたいと思います。
タクタイルスイッチを長押し判定は、やってみると意外と簡単!PHPだけで制御することができました。サンプルプログラムはこちらです。

/var/www/snooze_test.php

<?php
exec('echo 2 > /sys/class/gpio/export');
exec('echo in > /sys/class/gpio/gpio2/direction');

$count = 0;
$interval_sec = 0.25;
$txt = '一度もスイッチが押されませんでした';
for($i=0; $i<(10 / $interval_sec); $i++){
    $val = exec('cat /sys/class/gpio/gpio2/value');
    echo $i." : ".$count.PHP_EOL;
    if($val==1){
   	 $count += $interval_sec;
    }else if($count>0){
   	 $txt = 'スイッチが押されました';
   	 break;
    }
    if($count>=5){
   	 $txt = 'スイッチが長押しされました';
   	 break;
    }
    usleep($interval_sec*1000000);
}
echo $txt.PHP_EOL;
exec('echo 2 > /sys/class/gpio/unexport');

「スイッチが押された状態が一定時間連続した場合」という条件で、長押しかどうかを判定しています。上記のプログラムでは、5秒間スイッチを押し続けると長押し、5秒未満で放すと1プッシュ、感知されないまま10秒が経過するとそのまま終了します。

実行コマンド
php /var/www/snooze_test.php

押さずに10秒経過、1プッシュ、5秒長押しの順に、ボタンの状態を取得してみました。実際に試してみると、5秒の長押しは意外と長い!でも止めるのに多少手間がかかる方が二度寝防止になりそうなので、目覚ましとしてはこれで良いかもしれません。
この長押し判定を応用すれば、スヌーズ機能を実装できそうです!

 

任意のプロセスを終了させる方法

前回までに作成したプログラムでは、バックグラウンドで音を再生し、「killall」コマンドで強制終了させることで、「途中で音を止める」という処理を実現しています。「killall」コマンドはプロセス名を指定するため、同じ名前のプロセスがあった場合はすべて終了してしまいます。それはちょっと不安……ということで「killall」を使わずに、指定のプロセスだけを停止させる方法を試してみました。
任意のプロセスを指定して終了させるには、「kill」コマンドを使用します。

Linuxコマンド集 – 【 kill 】 プロセスおよびジョブを強制終了する:ITpro

kill プロセスID

このように、プロセスIDを引数として渡します。今回の場合は、このプロセスIDをPHP側で取得する必要があります。
試した結果、コマンド実行に使用している「exec」関数の第二引数を使って取得することができました。

PHP: exec – Manual
string exec ( string $command [, array &$output [, int &$return_var ]] )
引数 output が存在する場合、指定した配列は、 コマンドからの出力の各行で埋められます。 \n のような後に続く空白は、この配列には含まれません。

「exec」関数に第二引数を設定して実行すると、第一引数であるコマンドの出力結果を取得することができます。
テストプログラムで確認してみましょう。

/var/www/test_kill.php

<?php
$pid = array();
exec('mpg321 -l 0 -q /var/www/alerm1.mp3 > /dev/null & echo $!',$pid);

var_dump($pid);
sleep(10);

foreach ($pid as $no => $id) {
    echo $id . PHP_EOL;
    exec('sudo kill '.$id);
}

実行コマンド
php /var/www/test_kill.php

mpg321でアラーム音を10秒間鳴らし、プロセスIDを使って再生を停止するプログラムです。プロセスIDの取得は、3行目の「exec」関数内で行っています。
まず、第一引数(コマンド)に「echo $!」を追記しました。バックグラウンドで実行したコマンドのプロセスIDは「$!」に入ってくるので、その値を「echo」で出力しています。これで、プロセスIDを出力することができるので、「exec」関数の第二引数で値をキャッチするという流れです。
「var_dump」で「$pid」の中身を実際にのぞいてみると、

raspberrypi22_img01

図1

「3319」というプロセスIDをキャッチすることができました!このプログラムでは、キャッチしたプロセスIDをひとつひとつ「kill」コマンドで停止しています。
プロセスIDを使うことで、「killall」よりも丁寧に処理の停止ができるようになりました。

 

データの保存

寝ぼけて聞き逃してしまったときのための、データ保存機能!
第18回「ラズベリーパイで手作り温度計!」で作成した温度データのグラフ化プログラムを応用して、お天気情報やスヌーズ回数を記録に残してみましょう。
今回は文字情報が見やすいように、「Google Charts」のグラフの中から「Table」を選択しました。

Visualization: Table – Google Charts

タイトル行をクリックすると、昇順・降順の並べ替えができます。こうした動きのある機能が簡単に実装できるのがうれしいところですね!

グラフ化するデータはCSV出力しておくと仮定して、テストデータを準備します。今回は、「日付」「室温」「天気」「スヌーズ回数」の順に、4項目を用意しました。

/var/www/morning_data.csv

'2015/05/01',25.555,'晴れ',1
'2015/05/02',23.333,'曇り',2
'2015/05/03',21.111,'雨',3

次にプログラムの準備です。基本的な使い方はどのグラフも似ているので、以前作成した気温グラフ化のプログラムを少しカスタマイズするだけでOKです。

/var/www/morning_call_log.php

<?php
$csv_file	= './morning_data.csv';
$grapgh_data = '';

//CSV読み込み
if (($handle = fopen($csv_file, "r")) !== false) {
	while (($line = fgets($handle)) !== false) {
    	$grapgh_data .= '['.rtrim($line).'],'.PHP_EOL;
	}
	fclose($handle);
}else{
	echo 'no data';
}
?>
<html>
<head>
  <script type="text/javascript" src="https://www.google.com/jsapi"></script>
  <script type="text/javascript">
	google.load("visualization", "1", {packages:["table"]});
	google.setOnLoadCallback(drawTable);

	function drawTable() {
  	var data = new google.visualization.DataTable();
  	data.addColumn('string', 'Date');
  	data.addColumn('number', 'Temperature');
  	data.addColumn('string', 'Wether');
  	data.addColumn('number', 'Snooze');
  	data.addRows([

<?php echo $grapgh_data; ?>

  	]);

    	var table = new google.visualization.Table(document.getElementById('table_div'));
    	table.draw(data, {showRowNumber: true});
	}
  </script>
</head>
<body>
  <div id="table_div"></div>
</body>
</html>

ラズベリーパイのブラウザから「 http://localhost/morning_call_log.php 」、または、同ネットワークの端末から「 http://raspberrypi/morning_call_log.php 」のURLにアクセス!
実行結果は、

raspberrypi22_img02

図2

このようになります!
スヌーズ回数が多い日は、すっきり起きられなかった日。天気が悪い日や特定の曜日など、スヌーズ回数が多い日の傾向が分かれば、自分の苦手が発見できるかも!?

 

目覚まし時計プログラムと連携

スヌーズ機能は5分ごとに3回、アラームが鳴っているときに長押しされるとスヌーズ停止、という仕様にしてみました。

/var/www/morning_call.php

<?php
start();

/* アラーム */
function start(){
	$snooze_count = 3;		//スヌーズ回数
	$snooze_interval = 1;	//スヌーズ間隔(分)
	$stop_flg = false;
	$pid = array();

	if(checkCSV()==false) return;

	exec('echo 2 > /sys/class/gpio/export');
	exec('echo in > /sys/class/gpio/gpio2/direction');
	exec('mpg321 -l 0 -q /var/www/alerm1.mp3 > /dev/null & echo $!',$pid);
	$check = wait();
	foreach ($pid as $no => $id) {
		exec('sudo kill '.$id);
	}
	switch ($check){
		case 1:
			morning_call(true);
			break;
		case 0:
			morning_call(false);
			break;
		case -1:
			break;
		}
	exec('echo 2 > /sys/class/gpio/unexport');
}

/* ストップボタン待機 */
function wait(){
	$ret   = -1;
	$count = 0;
	$interval_sec = 0.25;	//スイッチのチェック間隔(秒)
	$wait_sec = 60;			//最大待機時間(秒)
	$snooze_sec = 5;		//スヌーズ判定時間(秒)
	for($i=0; $i<($wait_sec / $interval_sec); $i++){
		$val = exec('cat /sys/class/gpio/gpio2/value');
		echo $i.":".$count.PHP_EOL;

		if($val==1){
			//スイッチが押された
			$count += $interval_sec;
		}else if($count>0){
			//押下終了
			$ret = 0;
			break;
		}
		if($count>=$snooze_sec){
			//長押し
			$ret = 1;
			break;
		}
		usleep($interval_sec*1000000);
	}
	return $ret;
	//-1:押されなかった
	// 0:押された
	// 1:長押し
}

/* モーニングコール */
function morning_call($weather_flg=false){
	$weekday = array('日','月','火','水','木','金','土');
	$date = date('n月j日');
	$date_csv = date('Y/m/d');
	$dow  = $weekday[ date('w') ].'曜日';
	$time = date('G時i分');
	$txt  = 'おはようございます。'.$date.'、'.$dow.'、'.$time.'です。';
	$csv  = array('','','',1);
	$csv[0]= "'".$date_csv."'";
	$t    = get_temperature();
	if($t!=null){
		$txt .= '現在の室温は、'.$t.'度です。';
		$csv[1] = $t;
	}
	if($weather_flg){
		$w = get_weather();
		$txt .= $w['text'];
		$csv[2] = "'".$w['weather']."'";
	}
	echo $txt.PHP_EOL;
	echo strlen($txt).PHP_EOL;
	if(strlen($txt) > 980){
		$txt= mb_strcut($txt,0,980);
		$txt = substr($txt, 0, strrpos($txt, "。"));
		echo strlen($txt).PHP_EOL;
		echo $txt.PHP_EOL;
	}
	exec('/root/bin/jsay.sh '.$txt);
	writeCSV($csv);
	return;
}

/* 温度を取得 */
function get_temperature(){
	$deviceId    = '28-000006470bec';
	$sensor_path = '/sys/bus/w1/devices/'.$deviceId.'/w1_slave';
	$t = null;
	exec("cat ".$sensor_path, $w1_slave);
	if(isset($w1_slave[1])){
		$arr = explode('t=', $w1_slave[1]);
		if(isset($arr[1]))	$t = $arr[1] / 1000;
	}
	return $t;
}

/* 天気を取得 */
function get_weather(){
	$id   = '130010';
	$url  = 'http://weather.livedoor.com/forecast/webservice/json/v1?city='.$id;
	$json = @file_get_contents($url);
	$obj  = json_decode($json);
	$ret  = array('title'=>'', 'weather'=>'', 'description'=>'', 'text'=>'');

	if($obj==null){
	}else{
		if(isset($obj->title)){
			$ret['title'] = deleteWhiteSpace($obj->title);
			$ret['text'] .= $ret['title'].'は、';
		}
		if(isset($obj->forecasts[0]->telop)){
			$ret['weather'] = deleteWhiteSpace($obj->forecasts[0]->telop);
			$ret['text'] .= $ret['weather'].'です。';
		}
		if(isset($obj->description->text)){
			$ret['description'] = deleteWhiteSpace($obj->description->text);
			$ret['text'] .= $ret['description'];
		}
	}
	return $ret;
}
/* 改行・空白削除 */
function deleteWhiteSpace($txt){
	$txt = preg_replace('/(\s| )/','',$txt);
	$txt = preg_replace('/\n|\r|\r\n/', '', $txt);
	return $txt;
}
/* CSVチェック */
function checkCSV(){
	$csv_file = '/var/www/morning_data.csv';
	$date_csv = "'".date('Y/m/d')."'";
	$ret = true;
	if(file_exists($csv_file) && ($handle = fopen($csv_file, "r")) !== FALSE) {
		while (($line = fgetcsv($handle, 1000, ",")) !== false) {
			if($line[0]==$date_csv && $line[2]!=''){
				$ret = false;	//スヌーズ停止済み
				break;
			}
		}
		fclose($handle);
	}
	return $ret;
}
/* CSV書き込み */
function writeCSV($add_data){
	$csv_file = '/var/www/morning_data.csv';
	$csv_data = [];
	$flg = false;
	if(file_exists($csv_file) && ($handle = fopen($csv_file, "r")) !== FALSE) {
		while (($line = fgetcsv($handle, 1000, ",")) !== false) {
			if($line[0]==$add_data[0]){
				$line[1] = $add_data[1];
				$line[2] = $add_data[2];
				$line[3] = $line[3]+1;
				$flg = true;

			}
			$csv_data[] = $line;
		}
		fclose($handle);
	}
	if(!$flg){
		//追加
		$csv_data[] = $add_data;
	}
	if(($handle = fopen($csv_file, "w")) !== FALSE) {
		foreach ($csv_data as $data) {
			fputcsv( $handle, $data);
		}
		fclose($handle);
	}
	return;
}

1回目のアラームは通常の停止、2回目は長押ししてスヌーズも停止、という動きをテストしてみました。動画では、コマンドラインからプログラムを直接実行しています。この日はお天気情報が長文だったため、980バイト以内に調整された文章が読み上げられています。音声データの生成にも少々時間がかかっていますね。

スヌーズを停止すると天気を取得する仕様にしているので、天気の値(CSVの3列目の値)でスヌーズの状態を管理するようにしました。値が設定されていなければアラームを鳴らし、値が設定されている場合はスヌーズ停止済みのため、何もせずに終了します。

CSV出力が成功したかどうか、グラフ表示のプログラムを呼び出して確認してみましょう!

http://raspberrypi/morning_call_log.php

raspberrypi22_img03

図3

先程用意したテストデータの下に、新しいデータが追記されています。2回実行したので、「Snooze」列に「2」という値が入っていますね!

このプログラムを動かすために、目覚ましプログラム(morning_call.php)、グラフ表示プログラム(morning_call_log.php)、CSVファイル(morning_data.csv)の、3つのファイルを使用していますが、各ファイルの文字コードが異なっている場合、文字化けが発生してしまうことがあります。グラフがうまく表示されないときは、まずブラウザからHTMLソースを確認してみましょう。

ここまで来れば、グラフを追加したり、色をつけたり、プログラム側で自由にカスタマイズができますね!何秒でボタンを押したか、タイムトライアル風に記録をつけるのも面白そうです。

 

cronを再設定して最後の仕上げ!

スヌーズに対応できるように、cronの登録情報を変更します。ちょっと手間のかかる作業ではありますが、電子工作の醍醐味ということで、復習も兼ねて再設定!どうしても面倒という方は、PHP側で繰り返すような処理に書き換えてみてくださいね。

最初(第19回記事)に登録したcronは、「平日の6時45分に一度だけ実行」という命令でした。

45 6 * * 1-5 /root/morning.sh

5分ごとに3回実行したいので、50分と55分を追加するのが一番シンプルですね。

45,50,55 6 * * 1-5 /root/morning.sh

今回は目覚まし時計なので、鳴らしたい時間をそのまま列記する方がわかりやすいと思います。

曜日ごとに時間を変えてみたり、アラームの回数を増やしてみたりと、微調整しているうちにcronの勉強もできて一石二鳥!でも間違っていたら鳴らないかもしれない……電子工作ではじまる朝はちょっとスリリングですね。笑

 

まとめ

ストップボタン、日時・室温・天気予報の読み上げ、スヌーズ、データの保存……機能が盛りだくさんの目覚まし時計が完成しました!
今までの知識の総集編とも言えるラズベリーアラーム編、いかがでしたでしょうか?
ひとつひとつ覚えていくのも楽しいけれど、こうして応用できるようになると更に楽しい!作り込むごとにだんだん愛着がわいてきて、お気に入りの作品となりました!

次回は、新しいモノ作りに挑戦!
目覚まし時計では、データを受け取るためにAPIを使用しましたが、今度は逆に、こちらから情報を発信するようなツールを作成してみたいと思います。TwitterのAPIと連携して、自動でつぶやいてくれるbotのようなツールを手作りします!

高専ロボコン2016 出場ロボット解剖計画
円山ナカノ

プロフィール:プログラミング暦通算4年、最近IT業界に舞い戻ってきたプログラマーです。女子です。