前回はプログラムを起動すると、Webから情報取得→LED点滅パターンに変換→LED点滅制御、と一連の動作をプログラムにまとめました。今回は、設定した時刻にWebから情報を取得して、LED点滅パターンを更新してみます。
今回の説明内容
前回のプログラムでは、起動した時にWebから天気予報情報と鉄道運行状況の情報を取得してLED点滅制御しましたが、LEDの点滅制御が始まるとそのままでした。今回は、あらかじめ指定した時刻になったらWebから情報を再取得して情報更新、LED点滅パターン更新をする仕組みを実装してみます。
また、鉄道運行状況については最新の情報を知りたいケースもあると思いますので、スイッチを押したら情報更新するプログラムも追加してみます。
プログラム中に更新時刻をハードコードしてしまうと、あとで修正するのが面倒になりますので、時刻設定の仕様あたりから検討してみたいと思います。
今回は以下の内容で説明します。
- 更新時刻の指定方法検討
- 指定時刻になったらWebから情報再取得するアルゴリズムの検討
- 指定時刻情報更新の試作プログラム
- PHPで指定時刻に情報更新するプログラムを作成
- PHPのプログラムにスイッチを押したら情報更新するコードを追加
- Pythonで時刻の扱い方法検討
- Pythonで指定時刻に情報更新&スイッチを押したら更新するプログラムを作成
更新時刻の指定方法検討
特定の時刻になったらWebから情報を再取得する方法として、プログラム中のLEDを点滅制御するループ内で、指定時刻になったか確認し、指定時刻になったらWebから情報を再取得すればよさそうです。
ただ、具体的な指定時刻をループ内に記述してしまうと、あとからその時刻を変更するとき修正が面倒です。
そこで情報更新の時刻指定は、プログラムの最初に、PHPであれば配列、Pythonであればリストに指定しておく、という方法にしたいと思います。
例えばPHPであれば、
$timeToGet = ["06:30", "12:30", "17:30"];
というように、24時間指定で “hh:mm” (hは時、mは分) というフォーマットでプログラムの最初で指定しておくと、取得時刻の変更をする場合は便利ですよね。また、取得時刻を増やしたい場合も(減らしたい場合も)、以下のように単純に配列の要素を増やせばいいようにしておけば、今後のプログラムのメンテも楽になると思います。
$timeToGet = ["06:30", "12:30", "17:30", "20:30"];
ということで、時刻指定はプログラムの最初で配列(PHP)またはリスト(Python)で指定しておく方式にしたいと思います。
指定時刻になったらWebから情報再取得するアルゴリズムの検討
指定時刻になったらWebから情報を再取得するために、まずプログラム全体を整理しておきます。具体的には、情報を取得してLED点滅パターンに変換するコードを関数にしておき、更新時はその関数を呼び出すことにします。
点滅制御のループ内で情報の取得時刻になったかどうかのチェックをしますので、取得時刻になったらその関数を呼び出す、という実装にしようと思います。
イメージとしては以下のような感じです。
プログラムの最初で、取得時刻を配列に入れておきます。メイン処理では、LEDの点滅制御を繰り返しますので、LEDの点滅制御が1パターン分終わるごとに「次の取得時刻になったか」判定して、取得時刻になれば関数を呼び出して情報更新、LED点滅パターン更新すればOKそうです。
なんだか簡単そうですよね。言葉で書くのは簡単ですが、プログラムで書くのは意外に大変です。そこで、この判定については、先にPHPでアルゴリズムを検討、動作確認することにします。
「次の取得時刻になったか」の判定はどのように行えばいいでしょうか。この判定条件をもうちょっと正確に書くと「現在の時刻と次の取得時刻を比較して、現在の時刻が次の取得時刻を過ぎたか」という判定になります。プログラム的に書くと、
if( 現在時刻 > 次の取得時刻 ) {
情報更新;
}
という判定になります。
ところで、プログラム中、時刻はどのように扱うのでしょうか。プログラムでは、時刻を扱う一つの方法としてUNIX時間というものがあります。UNIX時間(UNIXタイムスタンプなどとも呼ばれています)とは、1970年1月1日の0時0分0秒を起点としてカウントした現時点の経過秒数になります。PHPでUNIX時間を取得する関数は time() です。以下のプログラムで現在のUNIX時間を表示することができます。
#!/usr/bin/php
<?php
echo time();
?>
例えばこの記事を書いている日時は、2019年5月12日の16時頃ですが、上のプログラムを実行すると、UNIX時間は
1557642910
と表示されました。15億5764万2910秒ということになります。なんか数字が大きすぎて感覚がよくわからないですよね。
このUNIX時間を使えば、情報を更新する部分のプログラムは以下のように記述できそうです。
if( time() > 次の取得時刻のUNIX時間 ) {
情報更新;
}
では「次の取得時刻のUNIX時間」を求めるにはどうしたらよいか、考えていきます。まず、取得時刻は以下のように準備しました。
$timeToGet = ["06:30", "12:30", "17:30"];
ということは、最初にこの配列から次の取得時刻を “hh:mm” 形式で見つけて、それをUNIX時間に変換すればよさそうです。おそらくそういう関数は用意されているはず、ということで検討を進めます。
それでは最初に、”hh:mm”形式で次に取得する時刻を探してみます。例えば現在の時刻が13:30の時を考えてみます。今が13:30であれば「次に取得する時刻」は “17:30” になります。この正解にたどり着く方法を検討してみましょう。
ひとつの方法として、”13:30″という文字列を、$timeToGet配列の要素の最初から、つまりインデックス0の要素から比較していけば求められそうです。やり方は、現在の時刻文字列が “13:30” ですので、まず$timeToGet[0]、つまり “06:30″と比較します。
(文字列の比較はアルファベット順の比較になります。例えば “12:00” は “11:00″より大きい、という感じで判定できます)
“13:30” と “06:30” では “13:30″の方が大きいので、”06:30” は次の取得時刻ではありません。次に “12:30″と比較しますが、これも “13:30″の方が大きいので次の取得時刻ではありません。”17:30″と比較すると、今度は”13:30″の方が小さいため、”17:30″が次の取得時刻、と判定できます。
では、現在の時刻が “20:00″ だったらどうでしょうか。比較していっても、$timeToGet配列には、”20:00” より大きい文字列はありません。この場合は取得時刻は配列最初の要素の “06:30” になります。ただし「翌日の」”06:30″が取得時刻になります。
この辺りにも注意すると、必要な関数は、
- UNIX時間から、通常の時刻表記、例えば “13:35” などの表記に変換する処理
- 翌日の日付を求める処理
が必要になります。ではそれぞれ確認していきましょう。
まず、現在の時刻を “hh:mm” 形式で取得したい場合、date()関数を使います。使い方は、
date(“文字列フォーマット”, UNIX時間);
です。date関数のすべてのフォーマット解説は長くなりますので省略しますが、UNIX時間を、24時間表記の “hh:mm” 形式で文字列に変換するには、
date("H:i", UNIX時間);
とします。現在のUNIX時間はtime()で取得できますので、現在の時間を “hh:mm”文字列にするには、
date("H:i", time());
とすればOKです。必要でしたら実際にRaspberry Piで動作確認してみてください。
また、date()関数ではUNIX時間から年月日の文字列も取得することが可能です。4桁の西暦で年月日を取得するには以下のようにします。
date("Y/m/d", time());
この関数を実行すると、例えば
2016/01/30
などの文字列が取得できます。
ところで、LED点滅中は現在のUNIX時間と次に取得するUNIX時間を比較することになるので、次に取得する時刻が “17:30” と分かったとしても、それをUNIX時間に変換しておく必要があります。
このような変換に対してもきちんと関数が用意されています。この変換は strtotime()関数で行います。パラメータは現在の時刻文字列です。例えば以下のように指定すると、指定した日時文字列に対するUNIX時間が取得できます。
strtotime("2016/01/30 12:57");
ところで、場合によっては翌日を求めることになりますが、これもstrtotime()関数でできてしまいます。時刻文字列の翌日を求めるには、以下のようにします。
strtotime("+1 day", UNIX時間);
例えば、以下のようにすると、現在の時刻の翌日の日時が求められます。うるう年とかの計算もしてくれるので助かりますね。
strtotime("+1 day", time());
さて、これで一通り必要な要素は揃いましたので、$timeToGet配列に取得時刻を設定して、このような考え方で次の取得時刻が取得できるか確認してみます。
指定時刻情報更新の試作プログラム
それでは、実際に確認用にプログラムを作成してみます。$timeToGet配列に、取得する時刻を入れておき、現在の時刻を取得して、次の取得時刻を算出する、という処理を行うプログラムになります。
特にプログラム中説明するところはないと思います。コメントを見ながら処理を追ってみてください。なお、現在の時刻を取得する処理は14行目、15行目ですが、デバッグ用に18行目で確認したい時刻を設定するようにしています。
本当の現在の時刻でテストしたい場合は、18行目をコメントアウトしてください。例えば今が “18:00” だったらどうなるか、などの確認をしたい場合は、18行目に確認したい時刻を設定します。
なお、制御ループは100回にしています。ループ1回で約3秒ですので、100回で300秒、つまり5分程度です。取得時刻に対して制御ループの動作時間が短いので、取得時刻に合わせて情報が更新されるのか確認したい場合は以下のようにデバッグするといいと思います。
例えば、現在の時刻が14:30である場合、プログラムを動作すると14:35ぐらいまで動きます。ということで,情報取得時刻を “14:32″、”14:34″の2つにしておきます。
このようにすると、プログラム動作開始時に1回情報取得、次に14:32になったら自動的に取得、さらに14:34になったらもう一回自動的に取得します。情報取得時にはデバッグ用に次に取得する時刻を表示するようにしていますが、14:34の取得時には、次の取得時刻は翌日の14:32になっているはずです。これが確認できればプログラム動作としては問題ないと思います。
#!/usr/bin/php
<?php
// 取得する時刻を配列に格納
$timeToGet = ["06:30", "12:30", "17:30"];
// 取得する時刻の数をカウント
$numOfTimeToGet = count($timeToGet);
// 次に取得する時刻を算出する
// 現在のUNIX時間を取得して"hh:mm"形式の文字列に変換
$currentTime = time();
$currentTimeStr = date("H:i", $currentTime);
// デバッグ用 - 現在の時刻を特定の時刻で上書き
$currentTimeStr = "08:00";
// デバッグ用 - 現在の時刻を表示
echo "現在の時刻: " . $currentTimeStr . "\n";
// 現在の時刻に一番近い、次に取得する時刻を $timeToGet 配列から探す
// 具体的には、最初に出現する、現在の時刻文字列より大きい文字列を探す
// このwhile文を抜けると、$indexに次に取得する時刻の配列番号がセットされる
$index = 0;
while( $currentTimeStr > $timeToGet[$index] ) {
$index++;
// もし$indexが配列要素数($numOfTimeToGet)に一致したら、
// 現在の時刻文字列より大きい時刻文字列は見つからなかったということなので、
// このwhile文を抜ける
if( $index == $numOfTimeToGet)
break;
}
// $indexが配列の要素数に等しい場合は、現在の時刻より遅い、次に取得する時刻はないので
// 翌日の配列最初の時刻が次の取得時刻になる
if( $index == $numOfTimeToGet ) {
$nextTimeIndex = 0;
// とりあえず本日の日付で取得日時の文字列を作成する
$nextTimeToGetStr = date("Y/m/d ", time()) . $timeToGet[0];
// その日時でUNIX時刻に変換する
$nextTimeToGet = strtotime($nextTimeToGetStr);
// UNIX時刻を1日進める
$nextTimeToGet = strtotime("+1 day", $nextTimeToGet);
$nextTimeToGetStr = date("Y/m/d H:i", $nextTimeToGet);
} else {
$nextTimeIndex = $index;
$nextTimeToGetStr = date("Y/m/d ", time()) . $timeToGet[$index];
$nextTimeToGet = strtotime($nextTimeToGetStr);
}
// デバッグ用 - 次に取得する時刻を表示
echo "次に取得する日時: " . $nextTimeToGetStr . "\n\n";
// この時点で、以下の情報がある
// $timeToGet配列: 取得する時刻
// $numOfTimeToGet: 取得する時刻の個数
// $nextTimeToGet: 次に取得する時刻(UNIX時間)
// $nextTimeIndex: 次に取得する時刻の$timeToGet配列インデックス
// あとは以下の処理を行えばよい
// 定期的に現在のUNIX時間と $nextTimeToGet の大小を比較する
// 現在のUNIX時間が $nextTimeToGet を過ぎたら、Webから情報を取得してLED点滅パターンを更新する
// $nextTimeIndexをプラス1する
// $nextTimeIndexが配列要素数($numOfTimeToGet)と一致したら $nextTimeIndex を 0 に戻す
// 次の取得時刻を $nextTimeToGet にセットする(翌日処理に注意)
?>
PHPで指定時刻に情報更新するプログラムを作成
これで、次に取得する時刻を求めることができましたので、本番プログラムに実装してみます。なお、デバッグ用に68行目、97行目、140〜142行目に取得時刻情報を表示するようにしています。またLED制御ループは10回繰り返すようにしています。
#!/usr/bin/php
<?php
// --- 個別設定(ここから)
// 天気予報情報の取得地域設定 ($cityに地域コードを設定)
$city = "140010"; // 横浜
// 鉄道運行状況情報取得する路線名設定
$line_name = "山手線";
// 取得する時刻を配列に格納
// 24時間表記で "hh:mm" 形式で記述する
$timeToGet = ["06:30", "12:30", "17:30"];
// --- 個別設定(ここまで)
// LED点滅パターンの基本パターンを連想配列に定義
$basic_pat = [
"on" => [1, 1, 1, 1], // 点灯したままのパターン
"off" => [0, 0, 0, 0], // 消灯したままのパターン
"blink" => [0, 1, 0, 1], // 点滅するパターン
];
// LEDのGPIO番号を連想配列に定義
$gpio_led = [
"fine" => 21, // 晴れ用LED
"cloud" => 20, // 曇り用LED
"rain" => 16, // 雨用LED
"snow" => 12, // 雪用LED
"rail_green" => 25, // 鉄道運行状況用 - 緑
"rail_blue" => 24, // 鉄道運行状況用 - 青
"rail_red" => 23, // 鉄道運行状況用 - 赤
];
// GPIOモードの設定
// $gpio_ledで定義したすべてのピンを出力モードに設定する
foreach($gpio_led as $led_name => $gpio_pin_number) {
`gpio -g mode $gpio_pin_number out`;
}
// LED点滅パターン用に空の配列を作成
$led_pat = [];
// 天気予報情報の取得とLED点滅パターンの設定
SetWeatherLedPattern();
// 鉄道運行状況情報の取得とLED点滅パターンの設定
SetRailwayInfoLedPattern();
// 次の取得時刻を $nextTimeToGet にUNIX時間で設定する
// --- 変数説明(ここから)
// $timeToGet: 取得時刻が hh:mm 形式で文字列として格納されている配列
// $numOfTimeToGet: 取得時刻の数
// $nextTimeIndex: 次の取得時刻の配列インデックス
// $nextTimeToGet: 次の取得時刻(UNIX時間)
// $nextTimeToGetStr: 次の取得時刻(yyyy/mm/dd hh:mm形式)
// --- 変数説明(ここまで)
// 取得する時刻の数をカウント
$numOfTimeToGet = count($timeToGet);
// 取得する時刻を算出する
// 最初に、現在の時刻を hh:mm 形式でUNIX時間から文字列に変換する
$currentTimeStr = date("H:i", time());
// デバッグ用 - 現在の時刻を表示
echo "現在の時刻: " . $currentTimeStr . "\n";
// 現在の時刻に一番近い、次に取得する時刻を $timeToGet 配列から探す
// このwhile文を抜けると、$indexに次に取得する時刻の配列番号がセットされることになる
$index = 0;
while( $currentTimeStr > $timeToGet[$index] ) {
$index++;
if( $index == $numOfTimeToGet)
break;
}
// $indexが配列の要素数に等しい場合は、現在の時刻より遅い、次に取得する時刻はないので
// 最初の時刻に戻す。ただし、日付は翌日になる
if( $index == $numOfTimeToGet ) {
$nextTimeIndex = 0;
// とりあえず本日の日付で取得日時の文字列を作成する
$nextTimeToGetStr = date("Y/m/d ", time()) . $timeToGet[0];
// その日時でUNIX時刻に変換する
$nextTimeToGet = strtotime($nextTimeToGetStr);
// UNIX時刻を1日進める
$nextTimeToGet = strtotime("+1 day", $nextTimeToGet);
$nextTimeToGetStr = date("Y/m/d H:i", $nextTimeToGet);
} else {
$nextTimeIndex = $index;
$nextTimeToGetStr = date("Y/m/d ", time()) . $timeToGet[$index];
$nextTimeToGet = strtotime($nextTimeToGetStr);
}
// デバッグ用 - 次に取得する時刻を表示
echo "次に取得する日時: " . $nextTimeToGetStr . "\n\n";
// メインループ
// 全体で100回繰り返す
for($repeat=0; $repeat<100; $repeat++) {
// LED表示バターン配列$led_patに入っている要素数分、点滅制御する
for($num=0; $num<count($led_pat["fine"]); $num++) {
// 各LEDの点灯・消灯の出力設定をする
foreach($gpio_led as $led_name => $gpio_pin_number) {
$value = $led_pat[$led_name][$num];
`gpio -g write $gpio_pin_number $value`;
}
// 0.25秒間待つ
usleep(250000);
}
// 取得時刻になったか確認して、取得時刻を過ぎていたら情報取得する
if( time() > $nextTimeToGet ){
// 天気予報情報の取得とLED点滅パターンの設定
SetWeatherLedPattern();
// 鉄道運行状況情報の取得とLED点滅パターンの設定
SetRailwayInfoLedPattern();
// 次の取得時刻を $nextTimeToGet にセットする
$nextTimeIndex++;
if( $nextTimeIndex == $numOfTimeToGet ) {
$nextTimeIndex = 0;
// とりあえず本日の日付で取得日時の文字列を作成する
$nextTimeToGetStr = date("Y/m/d ", time()) . $timeToGet[0];
// その日時でUNIX時刻に変換する
$nextTimeToGet = strtotime($nextTimeToGetStr);
// UNIX時刻を1日進める
$nextTimeToGet = strtotime("+1 day", $nextTimeToGet);
$nextTimeToGetStr = date("Y/m/d H:i", $nextTimeToGet);
} else {
$nextTimeToGetStr = date("Y/m/d ", time()) . $timeToGet[$nextTimeIndex];
$nextTimeToGet = strtotime($nextTimeToGetStr);
}
// デバッグ用
echo "情報を更新しました\n";
echo "次の取得時刻: " . $nextTimeToGetStr . "\n\n";
}
}
// GPIOピンの出力を0にする
foreach($gpio_led as $weather => $gpio) {
`gpio -g write $gpio 0`;
}
// --------- 関数 ---------
// 天気予報情報の取得とLED点滅パターンの設定関数
function SetWeatherLedPattern()
{
global $city;
global $basic_pat, $led_pat;
// 天気情報の取得URLを生成
$url = "https://weather.livedoor.com/forecast/webservice/json/v1?city=$city";
// 天気データ取得
// 最初に指定URLのデータを取得
$response = file_get_contents($url);
// jsonデータのためjson_decode関数で連想配列に変換
$weather = json_decode($response,true);
// 天気予報文字列の取得
$forecast = $weather["forecasts"][0]["telop"];
// デバッグ用
// 天気予報文字列の表示
echo "天気予報文字列: " . $forecast . "\n";
// 解析対象外の文字を削除
$forecast = str_replace(["大", "暴風", "雷"], "", $forecast);
// 「一時」を「時々」に置換
$forecast = str_replace("一時", "時々", $forecast);
// 「雨か雪」と「雪か雨」をそれぞれ「雨」と「雪」に変換
$forecast = str_replace("雨か雪", "雨", $forecast);
$forecast = str_replace("雪か雨", "雪", $forecast);
// 晴れの解析
// 最初に「晴」の完全一致を確認
if( strcmp($forecast, "晴れ") == 0 ) {
// 「晴れ」に完全一致していれば、晴のLEDは点灯したまま
$led_pat += [ "fine" => array_merge($basic_pat["on"], $basic_pat["on"], $basic_pat["on"]) ];
} else {
// 「晴れ」に完全一致していなければ、「晴」の文字が含まれているか確認
$pos = strpos($forecast, "晴");
if ( $pos !== false ) {
// 「晴」の文字が含まれている場合の処理
if ( $pos == 0 ) {
// 先頭に「晴」がある場合
$led_pat += [ "fine" => array_merge($basic_pat["on"], $basic_pat["off"], $basic_pat["off"]) ];
} else {
// 先頭にない場合は「時々晴」と「のち晴」を確認
if ( strpos($forecast, "時々晴") !== false ){
$led_pat += [ "fine" => array_merge($basic_pat["off"], $basic_pat["blink"], $basic_pat["off"]) ];
} else {
if ( strpos($forecast, "のち晴") !== false ){
$led_pat += [ "fine" => array_merge($basic_pat["off"], $basic_pat["on"], $basic_pat["off"]) ];
} else {
// いずれでもない場合は消灯にする
$led_pat += [ "fine" => array_merge($basic_pat["off"], $basic_pat["off"], $basic_pat["off"]) ];
}
}
}
} else {
// 予報文字列に晴れはないのでLEDは消灯したまま
$led_pat += [ "fine" => array_merge($basic_pat["off"], $basic_pat["off"], $basic_pat["off"]) ];
}
}
// 曇りの解析
if( strcmp($forecast, "曇り") == 0 ) {
$led_pat += [ "cloud" => array_merge($basic_pat["on"], $basic_pat["on"], $basic_pat["on"]) ];
} else {
$pos = strpos($forecast, "曇");
if ( $pos !== false ) {
if ( $pos == 0 ) {
$led_pat += [ "cloud" => array_merge($basic_pat["on"], $basic_pat["off"], $basic_pat["off"]) ];
} else {
if ( strpos($forecast, "時々曇") !== false ) {
$led_pat += [ "cloud" => array_merge($basic_pat["off"], $basic_pat["blink"], $basic_pat["off"]) ];
} else {
if ( strpos($forecast, "のち曇") !== false ) {
$led_pat += [ "cloud" => array_merge($basic_pat["off"], $basic_pat["on"], $basic_pat["off"]) ];
} else {
// いずれでもない場合は消灯にする
$led_pat += [ "cloud" => array_merge($basic_pat["off"], $basic_pat["off"], $basic_pat["off"]) ];
}
}
}
} else {
$led_pat += [ "cloud" => array_merge($basic_pat["off"], $basic_pat["off"], $basic_pat["off"]) ];
}
}
// 雨の解析
if( strcmp($forecast, "雨") == 0 ) {
$led_pat += [ "rain" => array_merge($basic_pat["on"], $basic_pat["on"], $basic_pat["on"]) ];
} else {
$pos = strpos($forecast, "雨");
if ( $pos !== false ) {
if ( $pos == 0 ) {
$led_pat += [ "rain" => array_merge($basic_pat["on"], $basic_pat["off"], $basic_pat["off"]) ];
} else {
if ( strpos($forecast, "時々雨") !== false ) {
$led_pat += [ "rain" => array_merge($basic_pat["off"], $basic_pat["blink"], $basic_pat["off"]) ];
} else {
if ( strpos($forecast, "のち雨") !== false ) {
$led_pat += [ "rain" => array_merge($basic_pat["off"], $basic_pat["on"], $basic_pat["off"]) ];
} else {
// いずれでもない場合は消灯にする
$led_pat += [ "rain" => array_merge($basic_pat["off"], $basic_pat["off"], $basic_pat["off"]) ];
}
}
}
} else {
$led_pat += [ "rain" => array_merge($basic_pat["off"], $basic_pat["off"], $basic_pat["off"]) ];
}
}
// 雪の解析
if( strcmp($forecast, "雪") == 0 ) {
$led_pat += [ "snow" => ["on", "on", "on"] ];
} else {
$pos = strpos($forecast, "雪");
if ( $pos !== false ) {
if ( $pos == 0 ) {
$led_pat += [ "snow" => array_merge($basic_pat["on"], $basic_pat["off"], $basic_pat["off"]) ];
} else {
if ( strpos($forecast, "時々雪") !== false ) {
$led_pat += [ "snow" => array_merge($basic_pat["off"], $basic_pat["blink"], $basic_pat["off"]) ];
} else {
if ( strpos($forecast, "のち雪") !== false ) {
$led_pat += [ "snow" => array_merge($basic_pat["off"], $basic_pat["on"], $basic_pat["off"]) ];
} else {
// いずれでもない場合は消灯にする
$led_pat += [ "snow" => array_merge($basic_pat["off"], $basic_pat["off"], $basic_pat["off"]) ];
}
}
}
} else {
$led_pat += [ "snow" => array_merge($basic_pat["off"], $basic_pat["off"], $basic_pat["off"]) ];
}
}
}
// 鉄道運行状況情報の取得とLED点滅パターンの設定関数
function SetRailwayInfoLedPattern()
{
global $line_name;
global $basic_pat, $led_pat;
// Yahoo! Japan運行情報のURL
// 以下は関東地方の運行情報URL
$url = "https://transit.yahoo.co.jp/traininfo/area/4/";
// Yahoo! Japan運行情報ページのHTMLデータ取得
// ただしテスト用なのでWebからの取得はせずにローカルに保存してあるファイルを読み込む
// $railway_html = file_get_contents($url);
$railway_html = file_get_contents("./index.html");
// 取得したHTMLデータを改行で分割して配列に格納
$railway_info = explode("\n", $railway_html);
// 配列に入れたHTMLデータ各行を解析
for($i=0; $i<count($railway_info)-1; $i++) {
// 情報取得する路線名文字列が含まれているかチェック
$pos = strpos($railway_info[$i], $line_name."</a></td>");
if ($pos !== false) {
// 含まれていれば次の行に運行情報文字列があるので
// 運行情報に合わせたLEDの色とパターンを格納
// まず「平常」が含まれるかチェック
$pos = strpos($railway_info[$i+1], "平常");
if($pos !== false) {
// 平常運転であればLED色は緑で点灯
$led_pat += [ "rail_green" => array_merge($basic_pat["on"], $basic_pat["on"], $basic_pat["on"]) ];
$led_pat += [ "rail_blue" => array_merge($basic_pat["off"], $basic_pat["off"], $basic_pat["off"]) ];
$led_pat += [ "rail_red" => array_merge($basic_pat["off"], $basic_pat["off"], $basic_pat["off"]) ];
// デバッグ用
echo "鉄道運行状況文字列: 平常\n";
} else {
// 平常運転でなければ「遅延」が含まれるかチェック
$pos = strpos($railway_info[$i+1], "遅延");
if($pos !== false) {
// 遅延であればLED色は黄色で点滅
$led_pat += [ "rail_green" => array_merge($basic_pat["blink"], $basic_pat["blink"], $basic_pat["blink"]) ];
$led_pat += [ "rail_blue" => array_merge($basic_pat["off"], $basic_pat["off"], $basic_pat["off"]) ];
$led_pat += [ "rail_red" => array_merge($basic_pat["blink"], $basic_pat["blink"], $basic_pat["blink"]) ];
// デバッグ用
echo "鉄道運行状況文字列: 遅延\n";
} else {
// 平常運転、遅延でもなければLEDを赤点滅して警告する
$led_pat += [ "rail_green" => array_merge($basic_pat["off"], $basic_pat["off"], $basic_pat["off"]) ];
$led_pat += [ "rail_blue" => array_merge($basic_pat["off"], $basic_pat["off"], $basic_pat["off"]) ];
$led_pat += [ "rail_red" => array_merge($basic_pat["blink"], $basic_pat["blink"], $basic_pat["blink"]) ];
// デバッグ用
echo "鉄道運行状況文字列: 異常(運行見合わせやその他)\n";
}
}
}
}
}
?>
PHPのプログラムにスイッチを押したら情報更新するコードを追加
鉄道運行状況情報は、出かける前などに最新の情報を知りたいこともあります。そこで、スイッチが押されたら鉄道運行状況情報のみを更新するようにしたいと思います。天気予報はそれほど変わることはないと思いますので、天気予報情報はあらかじめ設定した時刻になったら取得するようにします。
なお、変更点は少ないので、上のプログラムに対して、変更箇所のみ記載します。
まず、GPIO18番ピンをスイッチに接続していますので、GPIO18を入力に設定します。すでにGPIOピン設定している部分がありますので、そこにスイッチの設定を追加します。以下の7〜8行目を追加します。
// GPIOモードの設定
// $gpio_ledで定義したすべてのピンを出力モードに設定する
foreach($gpio_led as $led_name => $gpio_pin_number) {
`gpio -g mode $gpio_pin_number out`;
}
// スイッチピンは入力に設定する
`gpio -g mode 18 in`;
次にスイッチが押されたかの判定ですが、ちょっと感度は悪そうですが、0.25秒ウエイトするところに追加してみます。ここに追加すると、0.25秒ごとにスイッチ状態を確認することになりますので、スイッチを押している時間が短いと、この判定にかからない可能性があります。1秒間に4回スイッチ状態をチェックするので、普通にスイッチを押すのであれば、特に大きな問題はないと思います。
LED制御ループない、以下のコードを追加します。スイッチ状態を確認して、スイッチが押されていれば、鉄道運行状況情報を再取得します。
// LED表示バターン配列$led_patに入っている要素数分、点滅制御する
for($num=0; $num<count($led_pat["fine"]); $num++) {
// 各LEDの点灯・消灯の出力設定をする
foreach($gpio_led as $led_name => $gpio_pin_number) {
$value = $led_pat[$led_name][$num];
`gpio -g write $gpio_pin_number $value`;
}
// スイッチ状態を確認してスイッチが押されている場合は情報更新する
if( `gpio -g read 18` == 1 ) {
// 鉄道運行状況情報の取得とLED点滅パターンの設定
SetRailwayInfoLedPattern();
// デバッグ用
echo "スイッチが押されたので、鉄道運行状況情報を更新しました。\n\n";
}
// 0.25秒間待つ
usleep(250000);
}
これで基本的な動作を行うプログラムが完成しました!
実際に運用する場合、現在のfor文で指定回数実行しているところを、while(1)で無限ループにする必要があります。プログラムを停止させる場合は、Control + Cキーを押してプログラム実行を終了します。なお、この終了の仕方ですと、LEDの点滅制御中にプログラムが終わるため、LEDが点灯したまま、などの状況になる場合があります。
これを避けるために、プログラム終了手段の実装をチャレンジ課題にしたいと思います。
今回の時刻取得するところは、プログラミング経験がないとかなりややこしかったと思います。ただ、いろいろなプログラムを作っていくうちに慣れてくると思います。経験的に、書籍で勉強するより、具体的に何か作ってみる方がいろいろと身につくように思います。
この入門シリーズで作成するプログラムはここまでとして、残りの実装はチャレンジ課題にしたいと思います。
Pythonで時刻の扱い方法検討
PHPで時刻を扱ったときは、UNIX時間を基本にしましたが、Pythonでは日時を扱うためにdatetimeモジュールが用意されていますので、それを使用することにします。datetimeモジュールでは日時をdatetimeというオブジェクトとして扱います。ちょっとわかりづらいですが、実例を見ればすぐに見当がつくと思います。
Pythonで指定時刻になったら情報を更新するプログラムを作成する前に、PHPで行ったように、次の更新時刻を決定するテストプログラムを作って確認してみることにします。
先ほど説明したように、PHPではUNIX時間で処理しましたが、Pythonではdatetimeオブジェクトで時間を処理することにします。例えば現在の時刻は以下のように取得することができます。
currentTime = datetime.datetime.today()
これは、「datetime」モジュールの「datetime」オブジェクトの「today」メソッドを呼ぶ、という意味になります。このように取得したcurrentTimeもdatetimeオブジェクトになります。例えばcurrentTimeを “hh:mm”フォーマットの時刻表示にしたい場合は、datetimeオブジェクトに strftime というメソッドが用意されていますのでそれを使います。以下のようにすると、currentTimeを “hh:mm” フォーマットの時刻表示で表示することができます。
currentTime.strftime('%H:%M')
これから次の更新時刻を取得するテストプログラムを作ってみますが、もう一つ必要な変換がありますのでその方法を確認します。
PHPのテストプログラムでも、”2016-02-05 19:30″という文字列をUNIX時間に変換しました(strtotime関数)。これと同じ変換が必要になりますが、datetimeオブジェクトにはstrptimeメソッドが用意されていますので、それを使用します。
例えば、”2016-02-05 19:30″という文字列をdatetimeオブジェクトに変換するには、
datetime.datetime.strptime(時刻表現文字列, 時刻表元文字列を解釈するフォーマット)
とします。時刻表元文字列は例えば “2016-02-05 19:30” というものです。これを変換するのであれば、strptime(“2016-02-05 19:30”)でOKなような気もしますよね。でも日時の表現は国によっても異なります。ヨーロッパでは、年月日、ではなく日月年の表現です。例えば、西暦を2桁表示にして、”16-02-05″とした場合、2016年2月5日(日本式)なのか、2005年2月16日(ヨーロッパ表現)なのかわかりません。そのため、時刻表元文字列をどのように解釈するか、フォーマットを指定します。”2016-02-05 19:30″の日本式表現は以下のように指定します。
datetime.datetime.strptime("2016-02-05 19:30", '%Y-%m-%d %H:%M')
これで変換手法は揃いましたので、Pythonで次の時刻を取得する部分のテストプログラムを作成します。いかがテストプログラムで、PHPと対応させるように作成していますので、特に理解の難しいところはないと思います。
#!/usr/bin/python
# coding: utf-8
# モジュールインポート
import datetime
# 取得する時刻をタプルに格納
timeToGet = ('06:30', '12:30', '17:30')
# 取得する時刻の数をカウント
numOfTimeToGet = len(timeToGet)
# 次に取得する時刻を算出する
# 現在の時刻を取得して"hh:mm"形式の文字列に変換
currentTime = datetime.datetime.today()
currentTimeStr = currentTime.strftime('%H:%M')
# デバッグ用 - 現在の時刻を特定の時刻で上書き
currentTimeStr = '08:00';
# デバッグ用 - 現在の時刻を表示
print '現在の時刻: ' + currentTimeStr + '\n';
# 現在の時刻に一番近い、次に取得する時刻を timeToGet タプルから探す
# 具体的には、最初に出現する、現在の時刻文字列より大きい文字列を探す
# このwhile文を抜けると、indexに次に取得する時刻の配列番号がセットされる
index = 0;
while currentTimeStr > timeToGet[index]:
index += 1
if index == numOfTimeToGet:
break
# indexが配列の要素数に等しい場合は、現在の時刻より遅い、次に取得する時刻はないので
# 翌日の配列最初の時刻が次の取得時刻になる
today = currentTime.strftime('%Y-%m-%d ')
if index == numOfTimeToGet:
nextTimeIndex = 0;
# とりあえず本日の日付の取得時刻となるdatetimeオブジェクトを生成
nextTimeToGet = datetime.datetime.strptime(today + timeToGet[0], '%Y-%m-%d %H:%M')
# 翌日のdatetimeオブジェクトを生成
nextTimeToGet += datetime.timedelta(days=+1)
else:
nextTimeIndex = index
nextTimeToGet = datetime.datetime.strptime(today + timeToGet[index], '%Y-%m-%d %H:%M')
print nextTimeToGet
# この時点で、以下の情報がある
# timeToGet: 取得する時刻が格納されているタプル
# numOfTimeToGet: 取得する時刻の個数
# nextTimeToGet: 次に取得する時刻(datetimeオブジェクト)
# nextTimeIndex: 次に取得する時刻のtimeToGetのインデックス
# あとは以下の処理を行えばよい
# 定期的に現在時刻(datetime.datetime.today())と nextTimeToGet の大小を比較する
# 現在時刻が nextTimeToGet を過ぎたら、Webから情報を取得してLED点滅パターンを更新する
# nextTimeIndexをプラス1する
# nextTimeIndexがタプル要素数(numOfTimeToGet)と一致したら nextTimeIndex を 0 に戻す
# 次の取得時刻を nextTimeToGet にセットする(翌日処理に注意)
Pythonで指定時刻に情報更新&スイッチを押したら更新するプログラムを作成
Pythonのプログラムも、PHPに対応するように作成してみました。情報更新のアルゴリズムは上に示したプログラムをそのまま実装しています。
#!/usr/bin/python
# coding: utf-8
# モジュールインポート
import RPi.GPIO as GPIO
import time, datetime
import json, urllib2
# 天気予報情報の取得地域設定 ($cityに地域コードを設定)
city = '140010' #横浜
# 鉄道運行状況情報取得する路線名設定
lineName = '山手線'
# 取得する時刻をタプルに格納
timeToGet = ('06:30', '12:30', '17:30')
# LED点滅パターンの基本パターンを辞書に定義
basicPat = {
'on': (1, 1, 1, 1), # 点灯したままのパターン
'off': (0, 0, 0, 0), # 消灯したままのパターン
'blink': (0, 1, 0, 1) # 点滅するパターン
}
# LEDのGPIO番号を連想配列に定義
gpioLed = {
'fine': 21, # 晴れ用LED
'cloud': 20, # 曇り用LED
'rain': 16, # 雨用LED
'snow': 12, # 雪用LED
'rail_green': 25, # 鉄道運行状況用 - 緑
'rail_blue' : 24, # 鉄道運行状況用 - 青
'rail_red' : 23 # 鉄道運行状況用 - 赤
}
# GPIOモード設定
# GPIO番号指定をBCM(GPIO番号)に設定
GPIO.setmode(GPIO.BCM)
# gpioLedで定義したすべてのピンを出力モードに設定する
gpioPins = gpioLed.values()
GPIO.setup(gpioPins, GPIO.OUT)
# 取得する時刻の数をカウント
numOfTimeToGet = len(timeToGet)
# 次に取得する時刻を算出する
# 現在の時刻を取得して"hh:mm"形式の文字列に変換
currentTime = datetime.datetime.today()
currentTimeStr = currentTime.strftime('%H:%M')
# デバッグ用 - 現在の時刻を特定の時刻で上書き
currentTimeStr = '18:00'
# デバッグ用 - 現在の時刻を表示
print '現在の時刻: ' + currentTimeStr + '\n'
# 現在の時刻に一番近い、次に取得する時刻を timeToGet タプルから探す
# 具体的には、最初に出現する、現在の時刻文字列より大きい文字列を探す
# このwhile文を抜けると、indexに次に取得する時刻の配列番号がセットされる
index = 0;
while currentTimeStr > timeToGet[index]:
index += 1
if index == numOfTimeToGet:
break
# indexが配列の要素数に等しい場合は、現在の時刻より遅い、次に取得する時刻はないので
# 翌日の配列最初の時刻が次の取得時刻になる
today = currentTime.strftime('%Y-%m-%d ')
if index == numOfTimeToGet:
nextTimeIndex = 0
# とりあえず本日の日付の取得時刻となるdatetimeオブジェクトを生成
nextTimeToGet = datetime.datetime.strptime(today + timeToGet[0], '%Y-%m-%d %H:%M')
# 翌日のdatetimeオブジェクトを生成
nextTimeToGet += datetime.timedelta(days=+1)
else:
nextTimeIndex = index
nextTimeToGet = datetime.datetime.strptime(today + timeToGet[index], '%Y-%m-%d %H:%M')
# デバッグ用
# 次に取得する時刻を表示する
print '次に取得する時刻: '
print nextTimeToGet
# この時点で、以下の情報がある
# timeToGet: 取得する時刻が格納されているタプル
# numOfTimeToGet: 取得する時刻の個数
# nextTimeToGet: 次に取得する時刻(datetimeオブジェクト)
# nextTimeIndex: 次に取得する時刻のtimeToGetのインデックス
# あとは以下の処理を行えばよい
# 定期的に現在時刻(datetime.datetime.today())と nextTimeToGet の大小を比較する
# 現在時刻が nextTimeToGet を過ぎたら、Webから情報を取得してLED点滅パターンを更新する
# nextTimeIndexをプラス1する
# nextTimeIndexがタプル要素数(numOfTimeToGet)と一致したら nextTimeIndex を 0 に戻す
# 次の取得時刻を nextTimeToGet にセットする(翌日処理に注意)
# --------- 関数 ---------
# 天気予報情報の取得とLED点滅パターンの設定関数
def SetWeatherLedPattern():
global city
global basicPat, ledPat
# 天気情報の取得URLを生成
url = 'https://weather.livedoor.com/forecast/webservice/json/v1?city=%s' % city
try:
# 天気データ取得
# 最初に指定URLのデータ取得
response = urllib2.urlopen(url)
# jsonデータ取得
weather = json.loads(response.read())
# 天気予報文字列の取得
forecast = weather['forecasts'][0]['telop']
forecast = forecast.encode('utf-8')
finally:
response.close()
# 解析対象外の文字を削除
forecast = forecast.replace('大', '').replace('暴風', '').replace('雷', '')
# 「一時」を「時々」に置換
forecast = forecast.replace('一時', '時々')
# 「雨か雪」と「雪か雨」をそれぞれ「雨」と「雪」に置換
forecast = forecast.replace('雨か雪', '雨')
forecast = forecast.replace('雪か雨', '雪')
# 晴れの解析
# 最初に「晴れ」の完全一致を確認
if forecast == '晴れ':
# 「晴れ」に完全一致していれば、晴れのLEDは点灯したままのパターン
ledPat.update({'fine': basicPat['on'] + basicPat['on'] + basicPat['on']})
else:
# 「晴」の文字が含まれているか確認
pos = forecast.find('晴')
if pos >= 0:
# 「晴」が含まれている場合
if pos == 0:
# 先頭に「晴」がある場合
ledPat.update({'fine': basicPat['on'] + basicPat['off'] + basicPat['off']})
else:
# 先頭に「晴」がない場合は「時々晴」と「のち晴」を確認
if forecast.find('時々晴') > 0:
ledPat.update({'fine': basicPat['off'] + basicPat['blink'] + basicPat['off']})
else:
if forecast.find('のち晴') > 0:
ledPat.update({'fine': basicPat['off'] + basicPat['on'] + basicPat['off']})
else:
# いずれでもない場合は消灯する
ledPat.update({'fine': basicPat['off'] + basicPat['off'] + basicPat['off']})
else:
# 予報文字列に「晴」はないのでLEDは消灯
ledPat.update({'fine': basicPat['off'] + basicPat['off'] + basicPat['off']})
# 曇りの解析
if forecast == '曇り':
ledPat.update({'cloud': basicPat['on'] + basicPat['on'] + basicPat['on']})
else:
pos = forecast.find('曇')
if pos >= 0:
if pos == 0:
ledPat.update({'cloud': basicPat['on'] + basicPat['off'] + basicPat['off']})
else:
if forecast.find('時々曇') > 0:
ledPat.update({'cloud': basicPat['off'] + basicPat['blink'] + basicPat['off']})
else:
if forecast.find('のち曇') > 0:
ledPat.update({'cloud': basicPat['off'] + basicPat['on'] + basicPat['off']})
else:
ledPat.update({'cloud': basicPat['off'] + basicPat['off'] + basicPat['off']})
else:
ledPat.update({'cloud': basicPat['off'] + basicPat['off'] + basicPat['off']})
# 雨の解析
if forecast == '雨':
ledPat.update({'rain': basicPat['on'] + basicPat['on'] + basicPat['on']})
else:
pos = forecast.find('雨')
if pos >= 0:
if pos == 0:
ledPat.update({'rain': basicPat['on'] + basicPat['off'] + basicPat['off']})
else:
if forecast.find('時々雨') > 0:
ledPat.update({'rain': basicPat['off'] + basicPat['blink'] + basicPat['off']})
else:
if forecast.find('のち雨') > 0:
ledPat.update({'rain': basicPat['off'] + basicPat['on'] + basicPat['off']})
else:
ledPat.update({'rain': basicPat['off'] + basicPat['off'] + basicPat['off']})
else:
ledPat.update({'rain': basicPat['off'] + basicPat['off'] + basicPat['off']})
# 雪の解析
if forecast == '雪':
ledPat.update({'snow': basicPat['on'] + basicPat['on'] + basicPat['on']})
else:
pos = forecast.find('雪')
if pos >= 0:
if pos == 0:
ledPat.update({'snow': basicPat['on'] + basicPat['off'] + basicPat['off']})
else:
if forecast.find('時々雪') > 0:
ledPat.update({'snow': basicPat['off'] + basicPat['blink'] + basicPat['off']})
else:
if forecast.find('のち雪') > 0:
ledPat.update({'snow': basicPat['off'] + basicPat['on'] + basicPat['off']})
else:
ledPat.update({'snow': basicPat['off'] + basicPat['off'] + basicPat['off']})
else:
# 予報文字列に「雪」はないのでLEDは消灯
ledPat.update({'snow': basicPat['off'] + basicPat['off'] + basicPat['off']})
# 鉄道運行状況情報の取得とLED点滅パターンの設定関数
def SetRailwayInfoLedPattern():
global lineName
global basicPat, ledPat
# Yahoo! Japan運行情報ページのURL
# 以下は関東地方の運行情報URL
url = 'https://transit.yahoo.co.jp/traininfo/area/4/'
# Yahoo! Japan運行情報ページのHTMLデータ取得して1行ずつリストに格納
# response = urllib2.urlopen(url)
# htmlData = response.read()
# railwayInfo = htmlData.split('\n')
# ただしテスト用なのでWebからの取得はせずにローカルに保存してあるファイルを読み込む
# index.htmlをオープンして読み込み、1行ずつリストに格納
f = open('./index.html')
htmlData = f.read()
f.close()
railwayInfo = htmlData.split('\n')
# リストに入れたHTMLデータ各行を解析
for i in range(0, len(railwayInfo)):
# 情報取得する路線名文字列が含まれているかチェック
pos = railwayInfo[i].find(lineName + '</a></td>')
if pos >= 0:
# 含まれていれば次の行に運行情報文字列があるので
# 運行情報に合わせたLEDの色とパターンを格納
# まず「平常」が含まれるかチェック
pos = railwayInfo[i+1].find('平常')
if pos >= 0:
# 平常運転であればLED色は緑で点灯
ledPat.update({'rail_green': basicPat['on'] + basicPat['off'] + basicPat['off']})
ledPat.update({'rail_blue' : basicPat['off'] + basicPat['off'] + basicPat['off']})
ledPat.update({'rail_red' : basicPat['off'] + basicPat['off'] + basicPat['off']})
else:
# 平常運転でなければ「遅延」が含まれるかチェック
pos = railwayInfo[i+1].find('遅延')
if pos >= 0:
# 遅延であればLED色は黄色で点滅
ledPat.update({'rail_green': basicPat['blink'] + basicPat['blink'] + basicPat['blink']})
ledPat.update({'rail_blue' : basicPat['off'] + basicPat['off'] + basicPat['off']})
ledPat.update({'rail_red' : basicPat['blink'] + basicPat['blink'] + basicPat['blink']})
else:
# 平常運転、遅延でもなければLEDを赤点滅して警告する
ledPat.update({'rail_green': basicPat['off'] + basicPat['off'] + basicPat['off']})
ledPat.update({'rail_blue' : basicPat['off'] + basicPat['off'] + basicPat['off']})
ledPat.update({'rail_red' : basicPat['blink'] + basicPat['blink'] + basicPat['blink']})
# LED点滅処理
try:
# 点滅パターン格納用の辞書を初期化
ledPat = {}
# 天気予報情報の取得とLED点滅パターンの設定
SetWeatherLedPattern()
# 鉄道運行状況情報の取得とLED点滅パターンの設定
SetRailwayInfoLedPattern()
# 全体で3回繰り返す
for repeat in range(0, 3):
# 天気表示パターンledPatに入っている点滅パターンの要素数分、点滅制御する
for num in range(0, len(ledPat['fine'])):
# 各LEDの点灯・消灯の出力設定をする
for ledName, gpioPinNumber in gpioLed.items():
GPIO.output(gpioPinNumber, ledPat[ledName][num])
# 0.25秒待つ
time.sleep(0.25)
# 取得時刻になったか確認して、取得時刻を過ぎていたら情報取得する
if datetime.datetime.today() > nextTimeToGet:
# 点滅パターン格納用の辞書を初期化
ledPat = {}
# 天気予報情報の取得とLED点滅パターンの設定
SetWeatherLedPattern()
# 鉄道運行状況情報の取得とLED点滅パターンの設定
SetRailwayInfoLedPattern()
# 次の取得時刻を nextTimeToGet にセットする
# indexが配列の要素数に等しい場合は、現在の時刻より遅い、次に取得する時刻はないので
# 翌日の配列最初の時刻が次の取得時刻になる
today = currentTime.strftime('%Y-%m-%d ')
if index == numOfTimeToGet:
nextTimeIndex = 0
# とりあえず本日の日付の取得時刻となるdatetimeオブジェクトを生成
nextTimeToGet = datetime.datetime.strptime(today + timeToGet[0], '%Y-%m-%d %H:%M')
# 翌日のdatetimeオブジェクトを生成
nextTimeToGet += datetime.timedelta(days=+1)
else:
nextTimeIndex = index
nextTimeToGet = datetime.datetime.strptime(today + timeToGet[index], '%Y-%m-%d %H:%M')
# デバッグ用
# 次に取得する時刻を表示する
print '情報を更新しました。\n'
print '次の取得時刻: '
print nextTimeToGet
# ---------------
# GPIOリセット
# ---------------
finally:
GPIO.cleanup()
更新履歴
日付 | 内容 |
---|---|
2016.1.23 | 新規投稿 |
2019.5.12 | 誤記訂正 |