リアルタイムデータを活用したスマート信号機制御システム
部品と材料
| 部品 | 数量 | 備考 |
|---|---|---|
| Arduino Yun | 1 | メイン制御ボード(Linux搭載) |
| Arduino UNO | 1 | 送信側用(他のArduinoボードでも可) |
| 超音波距離センサー HC-SR04 | 1 | 車両と信号機の距離測定用 |
| DS3231 RTCモジュール | 1 | リアルタイムクロック(高精度時計) |
| LED(赤・黄・緑) | 各1 | 信号機表示用、330Ω抵抗とセットで使用 |
| 抵抗 330Ω | 3 | LED電流制限用 |
| 433MHz RFモジュール(送信機・受信機) | 1セット | Arduino間の無線通信用 |
| Androidデバイス | 1 | ドライバー向けアプリ実行用 |
| SDカード | 1 | PHPスクリプト格納用(Arduino Yun搭載スロット) |
| AllThingsTalk IOTOPIA 開発キット | 1 | オプション |
| ジャンパーワイヤー | 1セット | 配線用 |
アプリとオンラインサービス
- Arduino IDE - ファームウェア開発用
- Android Studio - スマートフォンアプリ開発用
- MySQLデータベースホスティングサービス - データ保存用(例:freemysqlhosting.net)
プロジェクト概要
イントロダクション
渋滞は誰もがイライラさせられる問題であり、年々深刻化しています。もしインテリジェントな信号機によってこの問題を解決できたらどうでしょうか? 私たちは「信号機情報システム」を開発しました。このシステムは、ドライバーが交差点で青信号を逃さず通過できる最適な速度を提示します(法定速度を超えない範囲で)。また、やむを得ず赤信号で停止する場合には、青信号に変わるまでの残り待ち時間を表示します。
信号機は「青信号の継続時間」や「次回青信号までの時間」などのデータをオンラインデータベースに送信します。スマートフォンのGPSやアプリがデータベースから情報を取得し、青信号通過に必要な速度を計算して表示します。赤信号で待たざるを得ない場合は、青信号までのカウントダウンタイマーを表示します。
動作例
制限速度70km/hの道路を走行中、前方の信号機が赤で、30秒後に青に変わるとします。あなたが信号機から500m手前にいる場合、システムは「60km/hで進行すれば青信号で通過できる」と推奨します。
プロジェクトデモンストレーション
システム構成と仕組み
デモンストレーション用に、Arduino Yunを用いた信号機モデルを作成しました。3つのLEDとRTCで構成され、設定されたインターバルで信号が切り替わります。Arduino Yunは青/赤信号の切り替わり時刻(Unixタイムスタンプ)を計算し、SDカード上のPHPスクリプトを実行してMySQLデータベースにデータを書き込みます。
Androidアプリ(Android Studioで開発)は、別のPHPスクリプト経由でJSON形式のデータを取得し、最適速度を計算します。青信号通過が不可能な場合はカウントダウンタイマーを表示します。
最適速度の計算には、車両と信号機の距離が必要です。本来はGPS座標から距離を算出しますが、スマートフォンのGPS精度では小規模な実証実験には不十分です。そこで、超音波測距モジュール(HC-SR04)を用い、433MHz RFモジュールでArduino Yunに距離データを送信する方式を採用しました。Yunは新しい測定値を受信するたびにデータベースを更新し、アプリは常に最新の距離情報を用いて計算を行います。
実装手順
- オンラインMySQLデータベースの作成
(本プロジェクトでは freemysqlhosting.net を使用) - テーブル 'TrafficL_data' の作成
スキーマはプロジェクトページの図を参照してください。 - 送信側ハードウェアの組み立て
回路図「Arduino transmitter」に従って配線します。 - 送信側Arduinoへスケッチ書き込み
- 超音波センサーで距離を測定
- 433MHz送信機でデータ送信
- Arduino YunをWi-Fiネットワークに接続
- Arduino YunのLinuxサーバーをPuttyで操作し、必要なパッケージをインストール
opkg update opkg install php5-mod-mysqli opkg install php5-cli nano /mnt/sda1/MySQL_UpdateTrafficLData.php'MySQL_UpdateTrafficLData.php' を配置し、実行権限を付与します。
nanoが見つからない場合はopkg install nanoでインストールしてください。chmod 755 /mnt/sda1/MySQL_UpdateTrafficLData.php /mnt/sda1/MySQL_UpdateTrafficLData.php nano /mnt/sda1/MySQL_UpdateDistance.php chmod 755 /mnt/sda1/MySQL_UpdateDistance.php /mnt/sda1/MySQL_UpdateDistance.php - 受信側ハードウェア(Arduino Yun)の組み立て
回路図「Arduino Yun receiver」に従って配線します。 - 受信側スケッチ 'Arduino Receiver & MySQLdb' を書き込み
- シリアルモニターで動作確認
正常に動作していれば、信号切り替え時刻と距離データが表示されます。 - Android Studioプロジェクトのセットアップ
- activity_main.xml
- MainActivity.java(URLを自分の環境に変更)
- AndroidManifest.xml
- strings.xml
- EchoJSON.php をサーバーに配置
- スマートフォンにアプリをインストール
ESP8266を用いた低コスト実装(追加情報)
より安価なESP8266ベースのモジュール(NodeMCU等)でArduino Yunを置き換えることも可能です。手順の概要は以下の通りです。
- 000webhost.com でアカウント作成・ウェブサイト作成・データベース作成
- CreateTable.php と PostDemo.php をファイルマネージャーでアップロードし、DB接続情報を設定
- CreateTable.php にアクセスしてテーブル作成
- Arduino IDEにESP8266ボードパッケージを追加(URL: https://arduino.esp8266.com/stable/package_esp8266com_index.json)
- ESP8266PostDemo.ino を書き込み(10秒ごとにランダム値でDB更新するデモコード)
- 動作確認後、Arduino Yun側の
MySQL_UpdateTrafficLData()とMySQL_UpdateDistance()をESP8266用に書き換え
注意: 一度に全てを実装せず、各コンポーネントを個別にテストしてから統合することを強く推奨します。質問はコメント欄で詳細に記述してください。
ソースコード
Arduino送信側スケッチ
/* IOTOPIA - 2016-2017 - Traffic Light Information System (Transmitter)
* by Pieter Luyten & Joppe Smeets
* RF 433MHzモジュールで距離データを別Arduinoへ送信
* RF tx_pin -> pin 3
* HC-SR04: trig_pin -> pin 5, echo_pin -> pin 6
*/
#define tx_pin 3
#define trig_pin 5
#define echo_pin 6
#include <VirtualWire.h>
#include "HCSR04.h"
HCSR04 ultrasonic(trig_pin, echo_pin);
int distance;
char CharMsg[21];
void setup() {
Serial.begin(9600);
vw_setup(2000);
vw_set_tx_pin(tx_pin);
}
void loop() {
distance = ultrasonic.Ranging(CM);
while(distance < 0) distance = ultrasonic.Ranging(CM);
sprintf(CharMsg, "%d,", distance);
vw_send((uint8_t *)CharMsg, strlen(CharMsg));
vw_wait_tx();
Serial.print("Distance (cm): ");
Serial.println(distance);
delay(250);
}
Arduino受信側スケッチ(Yun用・MySQL連携)
/* IOTOPIA - 2016-2017 - Traffic Light Information System (Receiver + MySQL)
* by Pieter Luyten & Joppe Smeets
* RF受信・信号機制御・MySQLアップロード
* RF rx_pin -> pin 9 (注: コメントと実装でpin番号が異なるため要確認)
* LED: Red=5, Orange=6, Green=7
* DS3231: SCL=pin3, SDA=pin2 (Yun)
* SDカード: Yun内蔵スロット
*/
#define RedLED 5
#define OrangeLED 6
#define GreenLED 7
#define rx_pin 9
#include <VirtualWire.h>
#include <TimeAlarms.h>
#include <Wire.h>
#include "RTClib.h"
#include <Process.h>
long UnixTime_green_1, UnixTime_red_1, UnixTime_green_2, UnixTime_red_2;
long s_till_orange;
RTC_DS3231 rtc;
char StringReceived[22];
boolean i;
int interval = 5;
int distance;
void setup() {
rtc.begin();
Serial.begin(9600);
Bridge.begin();
vw_setup(2000);
vw_set_rx_pin(rx_pin);
vw_rx_start();
Alarm.timerRepeat(interval, UpdateTrafficLData);
Alarm.timerRepeat(1, PrintCurrentTime);
pinMode(GreenLED, OUTPUT);
pinMode(OrangeLED, OUTPUT);
pinMode(RedLED, OUTPUT);
digitalWrite(GreenLED, LOW);
digitalWrite(OrangeLED, LOW);
digitalWrite(RedLED, LOW);
Serial.println("Setup finished");
}
void loop() {
Alarm.delay(0);
RF_Listen();
}
void RF_Listen() {
uint8_t buf[VW_MAX_MESSAGE_LEN];
uint8_t buflen = VW_MAX_MESSAGE_LEN;
if (vw_get_message(buf, &buflen)) {
for (int a = 0; a < buflen; a++) StringReceived[a] = char(buf[a]);
sscanf(StringReceived, "%d", &distance);
MySQL_UpdateDistance();
Serial.print("Distance: "); Serial.println(distance);
}
memset(StringReceived, 0, sizeof(StringReceived));
}
void UpdateTrafficLData() {
DateTime now = rtc.now();
if (i == 1) {
UnixTime_green_1 = now.unixtime() + interval + 1;
UnixTime_red_1 = now.unixtime() + 2 * interval + 1;
UnixTime_green_2 = now.unixtime() + 3 * interval + 1;
UnixTime_red_2 = now.unixtime() + 4 * interval + 1;
MySQL_UpdateTrafficLData(); i = 0;
} else {
UnixTime_red_1 = now.unixtime() + interval + 1;
UnixTime_green_1 = now.unixtime() + 2 * interval + 1;
UnixTime_red_2 = now.unixtime() + 3 * interval + 1;
UnixTime_green_2 = now.unixtime() + 4 * interval + 1;
MySQL_UpdateTrafficLData(); i = 1;
}
Serial.print(" Green_1 at "); Serial.print(UnixTime_green_1);
Serial.print(" Red_1 at "); Serial.print(UnixTime_red_1);
Serial.print(" Green_2 at "); Serial.print(UnixTime_green_2);
Serial.print(" Red_2 at "); Serial.println(UnixTime_red_2);
}
void MySQL_UpdateTrafficLData() {
long dataArray[4] = {UnixTime_green_1, UnixTime_red_1, UnixTime_green_2, UnixTime_red_2};
Process p;
p.begin("/mnt/sda1/MySQL_UpdateTrafficLData.php");
for (int b = 0; b < 4; b++) p.addParameter(String(dataArray[b]));
p.run();
while (p.available() > 0) Serial.print(p.read());
Serial.flush();
}
void MySQL_UpdateDistance() {
Process p;
p.begin("/mnt/sda1/MySQL_UpdateDistance.php");
p.addParameter(String(distance));
p.run();
while (p.available() > 0) Serial.print(p.read());
Serial.flush();
}
void PrintCurrentTime() {
DateTime now = rtc.now();
SetLEDsTrafficL();
Serial.print(now.hour()); Serial.print(':');
Serial.print(now.minute()); Serial.print(':');
Serial.print(now.second()); Serial.print(' ');
Serial.println(now.unixtime());
}
void SetLEDsTrafficL() {
DateTime now = rtc.now();
s_till_orange = UnixTime_red_1 - now.unixtime();
if (i == 0) { digitalWrite(GreenLED, LOW); digitalWrite(OrangeLED, LOW); digitalWrite(RedLED, HIGH); }
if (i == 1 && s_till_orange <= 3) { digitalWrite(GreenLED, LOW); digitalWrite(OrangeLED, HIGH); digitalWrite(RedLED, LOW); }
if (i == 1 && s_till_orange > 3) { digitalWrite(GreenLED, HIGH); digitalWrite(OrangeLED, LOW); digitalWrite(RedLED, LOW); }
}
MySQL_UpdateTrafficLData.php (SDカード上で実行)
#!/usr/bin/php-cli
<?php
$UnixTime_green_1 = $argv[1];
$UnixTime_red_1 = $argv[2];
$UnixTime_green_2 = $argv[3];
$UnixTime_red_2 = $argv[4];
$DBServer = 'yourServerName';
$DBUser = 'yourUserName';
$DBPass = 'yourPassword';
$DBName = 'yourDBName';
$conn = new mysqli($DBServer, $DBUser, $DBPass, $DBName);
if ($conn->connect_error) trigger_error('DB connection failed: ' . $conn->connect_error, E_USER_ERROR);
$sql = "UPDATE TrafficL_data SET UnixTime_green_1='$UnixTime_green_1', UnixTime_red_1='$UnixTime_red_1', UnixTime_green_2='$UnixTime_green_2', UnixTime_red_2='$UnixTime_red_2' WHERE id=1";
if ($conn->query($sql) === false) trigger_error('Wrong SQL: ' . $sql . ' Error: ' . $conn->error, E_USER_ERROR);
else echo "Data inserted!\n";
?>
MySQL_UpdateDistance.php
#!/usr/bin/php-cli
<?php
$Distance = $argv[1];
$DBServer = 'yourServerName';
$DBUser = 'yourUserName';
$DBPass = 'yourPassword';
$DBName = 'yourDBName';
$conn = new mysqli($DBServer, $DBUser, $DBPass, $DBName);
if ($conn->connect_error) trigger_error('DB connection failed: ' . $conn->connect_error, E_USER_ERROR);
$sql = "UPDATE TrafficL_data SET Distance='$Distance' WHERE id=1";
if ($conn->query($sql) === false) trigger_error('Wrong SQL: ' . $sql . ' Error: ' . $conn->error, E_USER_ERROR);
else echo "Distance inserted!\n";
?>
EchoJSON.php (Androidアプリ用JSON出力)
<?php
define('HOST', 'yourServerName');
define('USER', 'yourUserName');
define('PASS', 'yourPassword');
define('DB', 'yourDBName');
$con = mysqli_connect(HOST, USER, PASS, DB);
$sql = "select * from TrafficL_data";
$res = mysqli_query($con, $sql);
while($row = mysqli_fetch_array($res, MYSQLI_BOTH)){
printf('[ { "TrafficLArray": [ { "UnixTime_green_1":"%s", "UnixTime_red_1":"%s", "UnixTime_green_2":"%s", "UnixTime_red_2":"%s", "Distance":"%s" } ] } ]',
$row['UnixTime_green_1'], $row['UnixTime_red_1'], $row['UnixTime_green_2'], $row['UnixTime_red_2'], $row['Distance']);
}
mysqli_close($con);
?>
MainActivity.java (Androidアプリメインロジック)
package com.example.xxx.xxx; // パッケージ名を変更
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonArrayRequest;
import com.android.volley.toolbox.Volley;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class MainActivity extends AppCompatActivity {
TextView results, recSpeed;
String JsonURL = "YourURLhere";
RequestQueue requestQueue;
private final static double maxSpeed = 18; // cm/s
private double speed;
public final static int NUMBER_OF_ENTRIES = 2;
long[] unixTimesGreen = new long[NUMBER_OF_ENTRIES];
long[] unixTimesRed = new long[NUMBER_OF_ENTRIES];
private double distance;
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private Future<?> timingTask;
public void tick(long milliseconds) {
timingTask = scheduler.scheduleAtFixedRate(() -> updateTable(), 0, milliseconds, TimeUnit.MILLISECONDS);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
results = findViewById(R.id.jsonData);
recSpeed = findViewById(R.id.recommendedSpeed);
updateTable();
tick(500);
}
private void updateTable() {
requestQueue = Volley.newRequestQueue(this);
JsonArrayRequest arrayreq = new JsonArrayRequest(JsonURL,
response -> {
try {
JSONObject TrafficLObj = response.getJSONObject(0);
JSONArray trafficArry = TrafficLObj.getJSONArray("TrafficLArray");
for (int i = 0; i < trafficArry.length(); i++) {
JSONObject jsonObject = trafficArry.getJSONObject(i);
String[] green = new String[NUMBER_OF_ENTRIES];
String[] red = new String[NUMBER_OF_ENTRIES];
String distanceTemp = jsonObject.getString("Distance");
distance = (double) Long.parseLong(distanceTemp);
green[0] = jsonObject.getString("UnixTime_green_1");
red[0] = jsonObject.getString("UnixTime_red_1");
green[1] = jsonObject.getString("UnixTime_green_2");
red[1] = jsonObject.getString("UnixTime_red_2");
for (int j = 0; j < unixTimesRed.length; j++) {
unixTimesGreen[j] = Long.parseLong(green[j]);
unixTimesRed[j] = Long.parseLong(red[j]);
}
}
updateTextView();
} catch (JSONException e) { e.printStackTrace(); }
},
error -> Log.e("Volley", "Error")
);
requestQueue.add(arrayreq);
}
private void updateTextView() {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < unixTimesGreen.length; i++) {
builder.append("unixTimeGreen").append(i).append(": ").append(new Date(unixTimesGreen[i]*1000)).append("\n");
builder.append("unixTimeRed").append(i).append(": ").append(new Date(unixTimesRed[i]*1000)).append("\n");
}
builder.append("distance: ").append(distance);
speedCalc();
recSpeed.setText(String.format("%.0f", speed));
}
private void speedCalc() {
int interval = 0;
double time = System.currentTimeMillis() / 1000.0;
for (int i = 0; i < NUMBER_OF_ENTRIES; i++) {
double diff = unixTimesRed[interval] - time;
speed = distance / (diff / 1000);
interval++;
}
interval--;
if (distance / (unixTimesGreen[interval] - time) > maxSpeed) speed = maxSpeed;
else speed = distance / (unixTimesGreen[interval] - time);
speed *= 5; // 表示用スケーリング
}
}
AndroidManifest.xml / strings.xml / activity_main.xml
プロジェクトページのコードセクションを参照してください。strings.xml のアプリ名は「NooitRood」(オランダ語で「Never Red=赤信号なし」の意)です。
回路図
送信側 (Arduino UNO + HC-SR04 + 433MHz送信機)
- 433MHz tx_pin → pin 3
- HC-SR04 trig_pin → pin 5, echo_pin → pin 6
受信側 (Arduino Yun + 433MHz受信機 + LED信号機 + DS3231)
- 433MHz rx_pin → pin 9
- LED: Red→pin5, Orange→pin6, Green→pin7
- DS3231: SCL→pin3, SDA→pin2 (Yun)
製造プロセス