---
# System prepended metadata

title: ハンズオン：バス乗車時行先通知システムの開発
tags: [Web×IoTメイカーズチャレンジ]

---

---
title: ハンズオン：バス乗車時行先通知システムの開発
tags: Web×IoTメイカーズチャレンジ
slideOptions:
    theme: white
    slideNumber: 'c/t'
    center: false
    transition: 'none'
    keyboard: true
    width: '93%'
    height: '100%'
---

<style>
/* basic design */
.reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6,
.reveal section, .reveal table, .reveal li, .reveal blockquote, .reveal th, .reveal td, .reveal p {
    font-family: 'Meiryo UI', 'Source Sans Pro', Helvetica, sans-serif, 'Helvetica Neue', 'Helvetica', 'Arial', 'Hiragino Sans', 'ヒラギノ角ゴシック', YuGothic, 'Yu Gothic';
    text-align: left;
    line-height: 1.8;
    letter-spacing: normal;
    text-shadow: none;
    word-wrap: break-word;
    color: #444;
}
.reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 {font-weight: bold;}
.reveal h1, .reveal h2, .reveal h3 {color: #2980b9;}
.reveal th {background: #DDD;}
.reveal section img {background:none; border:none; box-shadow:none; max-width: 95%; max-height: 95%;}
.reveal blockquote {width: 90%; padding: 0.5vw 3.0vw;}
.reveal table {margin: 1.0vw auto;}
.reveal code {line-height: 1.2;}
.reveal p, .reveal li {padding: 0vw; margin: 0vw;}
.reveal .box {margin: -0.5vw 1.5vw 2.0vw -1.5vw; padding: 0.5vw 1.5vw 0.5vw 1.5vw; background: #EEE; border-radius: 1.5vw;}
/* table design */
.reveal table {background: #f5f5f5;}
.reveal th {background: #444; color: #fff;}
.reveal td {position: relative; transition: all 300ms;}
.reveal tbody:hover td { color: transparent; text-shadow: 0 0 3px #aaa;}
.reveal tbody:hover tr:hover td {color: #444; text-shadow: 0 1px 0 #fff;}
/* blockquote design */
.reveal blockquote {
    width: 90%;
    padding: 0.5vw 0 0.5vw 6.0vw;
    font-style: italic;
    background: #f5f5f5;
}
.reveal blockquote:before{
    position: absolute;
    top: 0.1vw;
    left: 1vw;
    content: "\f10d";
    font-family: FontAwesome;
    color: #2980b9;
    font-size: 3.0vw;
}
/* font size */
.reveal h1 {font-size: 5.0vw;}
.reveal h2 {font-size: 4.0vw;}
.reveal h3 {font-size: 2.8vw;}
.reveal h4 {font-size: 2.6vw;}
.reveal h5 {font-size: 2.4vw;}
.reveal h6 {font-size: 2.2vw;}
.reveal section, .reveal table, .reveal li, .reveal blockquote, .reveal th, .reveal td, .reveal p {font-size: 2.2vw;}
.reveal code {font-size: 1.6vw;}
/* new color */
.red {color: #EE6557;}
.blue {color: #16A6B6;}
/* split slide */
#right {left: -18.33%; text-align: left; float: left; width: 50%; z-index: -10;}
#left  {left:  31.25%; text-align: left; float: left; width: 50%; z-index: -10;}
</style>

<style>
/* specific design */
.reveal h1 {
    margin: 0% -100%;
    padding: 2% 100% 4% 100%;
	color: #fff;
    background: #c2e59c;  /* fallback for old browsers */
	background: linear-gradient(-45deg, #EE7752, #E73C7E, #23A6D5, #23D5AB);
	background-size: 200% 200%;
	animation: Gradient 60s ease infinite;
}
@keyframes Gradient {
	0%   {background-position: 0% 50%}
	50%  {background-position: 100% 50%}
	100% {background-position: 0% 50%}
}
.reveal h2 {
    text-align: center;
    margin: -5% -50% 2% -50%;
    padding: 4% 10% 1% 10%;
    color: #fff;
    background: #c2e59c;  /* fallback for old browsers */
    background: -webkit-linear-gradient(to right, #64b3f4, #c2e59c);  /* Chrome 10-25, Safari 5.1-6 */
    background: linear-gradient(to right, #64b3f4, #c2e59c); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
}
</style>


<!-- --------------------------------------------------------------------------------------- -->

## システム内製開発ハンズオン
#  乗車時行先案内システム
#### 2025年10月18日（土）

---

## システム開発手順の確認
Step1：Raspberry Pi Zero（以下，「Pi Zero」と呼称）と Bluetoothスピーカとを接続させる
Step2：Pi Zero において音声合成ソフトを動作させ、テキストから音声ファイルを生成させる
Step3：Pi Zero において生成された音声ファイルをBluetoothスピーカに再生させる

---

## Pi Zeroへのログイン
https://chirimen.org/pizero/  のステップ１を参考に作業をおこないます．
(1) CHIRIMEN Liteを書き込んだMicroSDカードをPi Zeroに差し込む
(2) PCのUSBとPi ZeroのUSB OTGポートをUSBケーブルでつなぐ
(3) ターミナルウィンドウ（ https://www.chirimen.org/PiZeroWebSerialConsole/PiZeroWebSerialConsole.html ）を開く
(4) ターミナルウィンドウの[ Connect and Login PiZero ]ボタンを押し，「接続」をクリックする
(5) コンソールにコマンドプロンプト「pi@raspberrypi:~$」の表示を確認する

---

## Pi ZeroのWi-Fi設定
https://chirimen.org/pizero/ のステップ２ を参考に作業をおこないます．
(1) ターミナルウィンドの[wifi panel]ボタンを押す
(2) ウィンド下部に、WiFiアクセス情報（SSID，PASS PHRASE）を入力する
(3) [SET WiFi]ボタンを押す
(4) [Reboot]ボタンを押す
(5) ターミナルウィンドウの[Close Connection]ボタンを押す
(6) Pi Zeroが再起動する間（1〜2分）待ち，再び[Connect and Login PiZero]ボタンを押して接続する
(7) [Wifi panel]ボタンを再び押す
(8) [Wifi Info]ボタンを押し，IP Addressが設定されていればWifiに接続成功している
(9) WiFi設定を確認できたらWiFi Settingウィンドウを閉じる

---

## パッケージ管理ツール
### パッケージ管理
実行ファイルや設定ファイルやライブラリを一括で管理することを指す

### APT
Raspberry Pi OS のパッケージ管理ツールはAPTです．APTについては以下を参照してください．
https://ja.wikipedia.org/wiki/APT

#### APTのパッケージ一覧を更新する
```bash:
pi@raspberrypi:~/myApp$ sudo apt-get update
```

---

## Bluetoothの設定
#### 参考にした資料（必要に応じて参照）
https://www.mikan-tech.net/entry/raspi-bluetooth-speaker

#### 事前準備
##### 必要なパッケージのインストール
PulseAudioとPulseAudioのBluetoothスピーカーのモジュールを使う．
PulseAudioとは，Linuxにて使われているオーディオ信号の経路制御や音量制御、ミキシング等を行うための裏方ツール。
```bash:
pi@raspberrypi:~/myApp$ sudo apt-get install pulseaudio pulseaudio-module-bluetooth
```

##### PulseAudioの設定
- テキスト編集ソフトで設定ファイル（/etc/pulse/default.pa）を編集します。
```bash:
pi@raspberrypi:~/myApp$ sudo nano /etc/pulse/default.pa
```

- ファイルの末尾に次の行をコピペする
```bash=
# automatically switch to newly-connected devices
load-module module-switch-on-connect
```

- 保存する
    - Ctrl+Xを押す
    - Yを押す
    - ファイル名を確認し，ENTERキーを押す

##### 権限を追加する
- 現在ログインしているアカウントがbluetoothを操作できるように設定する
```bash=
pi@raspberrypi:~/myApp$ sudo usermod -G bluetooth -a pi
```
- 反映には再起動が必要なので、ここで再起動します。
```bash=
pi@raspberrypi:~/myApp$ sudo reboot
```

#### ペアリング
##### PulseAudioの起動
Bluetoothスピーカーをペアリングするには、pulseaudioが起動していなければなりません。 次のコマンドで起動できます。
```bash=
pi@raspberrypi:~/myApp$ pulseaudio --start
```

##### Bluetoothのスキャン

```bash=
pi@raspberrypi:~/myApp$ bluetoothctl
```

起動すると、[blueooth] #というプロンプトが出てきて、コマンドを受け付けます。 次のコマンドで、スキャンができます。
```bash=
[bluetooth]# power on
[bluetooth]# scan on
```

##### ペアリング
```bash=
[bluetooth]# pair XX:XX:XX:XX:XX:XX
```

Pairing successfulと出れば成功です。ついでにtrustコマンドでTrustリストに登録しておきます。
```bash=
[bluetooth]# trust XX:XX:XX:XX:XX:XX
```

最後に、ペアリングしたスピーカーに接続します。connectコマンドを使います。
```bash=
[bluetooth]# connect XX:XX:XX:XX:XX:XX
```

スピーカーから接続音が鳴り、接続ができました。プロンプトも[CLS-20]#に変わりました。
```bash=
[CLS-20]# quit
```

##### 動作チェック
音を鳴らしてみましょう。
```bash=
$ aplay /usr/share/sounds/alsa/Front_Center.wav
```

---

## 音声合成
#### 参考資料
https://raspida.com/speech-synthesis/
https://blog.serverworks.co.jp/tech/2017/08/09/pi-zero-mmd/

#### 音声合成ソフト
Open JTalk を使います．
Open JTalk」のメリットは軽量であることです．

#### Open JTalk のインストール
- 音声合成ソフト Open JTalkをインストール
```bash:
pi@raspberrypi:~/myApp$ sudo apt install open-jtalk
```
- 音声辞書をインストール（例：宇多津→ウタヅ）
```bash:
pi@raspberrypi:~/myApp$ sudo apt install open-jtalk-mecab-naist-jdic
```
- 音声エージェント（話者）をインストール
```bash:
pi@raspberrypi:~/myApp$ sudo apt install hts-voice-nitech-jp-atr503-m001
```

#### 辞書のインストール
##### 辞書をインストールする先のフォルダを作る
```bash:
pi@raspberrypi:~/myApp$ sudo mkdir -p /usr/share/open_jtalk/dic
```

##### 別の場所から辞書ファイルをダウンロード（GitHubからの例）
```bash:
pi@raspberrypi:~/myApp$ sudo wget https://github.com/r9y9/open_jtalk/releases/download/v1.11.1/open_jtalk_dic_utf_8-1.11.tar.gz
```

##### ダウンロードしたファイルを解凍して指定のディレクトリに配置
```bash:
pi@raspberrypi:~/myApp$ sudo tar -xzf open_jtalk_dic_utf_8-1.11.tar.gz -C /usr/share/open_jtalk/dic --strip-components=1
```

#### Pi Zero に発声させるプログラムを作成する
- パッケージを更新する
```bash:
pi@raspberrypi:~/myApp$ sudo apt update
```

- myAppフォルダ配下にtalkフォルダを作成する
```bash:
pi@raspberrypi:~/myApp$ mkdir talk
```
- talk フォルダに移動する
- Node.js の動作に必要なモジュールを準備する
```bash:
pi@raspberrypi:~/myApp/talk $ npm init -y
```


- 「app.mjs」というファイルを作成し，以下の内容をコピペする
```js
import { exec } from 'child_process';
//const { exec } = require('child_process');

// 合成したいテキスト
const text = 'こんにちは、ラズベリーパイからのメッセージです。';
// 出力する音声ファイルの名前
const outputWav = 'output.wav';

// Open JTalkで音声合成する関数
function synthesizeVoice(text, callback) {
  const command = `echo "${text}" | open_jtalk -x /usr/share/open_jtalk/dic -m /usr/share/hts-voice/nitech-jp-atr503-m001/nitech_jp_atr503_m001.htsvoice -ow ${outputWav}`;

  exec(command, (error, stdout, stderr) => {
    if (error) {
      console.error(`音声合成中にエラーが発生しました: ${error.message}`);
      return;
    }
    console.log('音声合成が完了しました。');
    callback();
  });
}

function playVoice() {
  exec(`aplay ${outputWav}`, (error) => {
    if (error) {
      console.error(`音声再生中にエラーが発生しました: ${error.message}`);
    }
    console.log('音声が再生されました。');
  });
}

// 実行
synthesizeVoice(text, playVoice);
```

- プログラムを実行する
```bash:
pi@raspberrypi:~/myApp/talk $ node app.js
```

#### 女性の音声エージェントを使って合成してみよう
次に、女性の音声エンジンを使用するため、「MMDAgent」を利用します。MMDAgentとは、名古屋工業大学国際音声技術研究所によって作成されたオープンソースの音声インタラクションシステム構築ツールキットです。

以下URLから女性の音声データを以下の手順でダウンロードします。ダウンロードが完了したら、MMDAgent_Example-1.7.zip を展開し、meiディレクトリ配下を /usr/share/hts-voice/ にコピーします。

```bash:
pi@raspberrypi:~/myApp/talk $ wget http://downloads.sourceforge.net/project/mmdagent/MMDAgent_Example/MMDAgent_Example-1.7/MMDAgent_Example-1.7.zip
pi@raspberrypi:~/myApp/talk $ unzip MMDAgent_Example-1.7.zip
pi@raspberrypi:~/myApp/talk $ sudo cp -R ./MMDAgent_Example-1.7/Voice/mei /usr/share/hts-voice/
```

app.jsを開き，音声エージェントを変更します．
```js:
-m /usr/share/hts-voice/nitech-jp-atr503-m001/nitech_jp_atr503_m001.htsvoice
```
の部分を以下に置き換えます．
```js:
-m /usr/share/hts-voice/mei/mei_normal.htsvoice
```

- 再度，プログラムを実行します．
```bash:
pi@raspberrypi:~/myApp/talk $ node app.mjs
```

---

## Web APIからバスのデータを取得してみよう
#### アプリフォルダを作成する
- myAppフォルダに移動します．
- busNotification フォルダを作成します．
```bash:
pi@raspberrypi:~/myApp$ mkdir busNotification
```

#### プログラムを作成する
- busNotification フォルダに移動します．
- Node.js の動作に必要なモジュールを準備する
```bash:
pi@raspberrypi:~/myApp/busNotification $ npm init -y
```
- 必要なモジュールを本フォルダにインストールします．
```bash=
pi@raspberrypi:~/myApp/busNotification$ npm install https
```

- 「app.mjs」というファイルを作成し，以下のJavaScriptプログラムをコピペしてください．

```js
import https from 'https';
//const https = require('https');

const url = new URL("https://komelabo.sakura.ne.jp/kanonji/api/orion.php");

const req = https.request(url, res => {
  let data = '';

  console.log(`statusCode: ${res.statusCode}`);

  // データのバッファリング
  res.on('data', chunk => {
    data += chunk;
  });

  // データの受信が完了したら出力
  res.on('end', () => {
    console.log(data);
  });
});

req.on('error', error => {
  console.error(`リクエストエラー: ${error.message}`);
});

req.end();
```

#### プログラムを実行する
```bash=
node app.mjs 
```

- typeを指定してデータを取得する．app.mjsのURLを以下のように修正する．
```js
https://komelabo.sakura.ne.jp/kanonji/api/orion.php
↓
https://komelabo.sakura.ne.jp/kanonji/api/orion.php?type=Bus
```

- idを指定してデータを取得する．app.mjsのURLを以下のように修正する．
```js
https://komelabo.sakura.ne.jp/kanonji/api/orion.php?type=Bus
↓
https://komelabo.sakura.ne.jp/kanonji/api/orion.php?type=Bus&id=jp.kagawa.kanonji.Bus.33
```

---

## Web APIから取得したデータに基づいてスマートスピーカーに案内を出させてみよう
- app.mjs を以下のJavaScriptプログラムに書き換えます．
```js:
import { exec } from 'child_process';
import fs from 'fs';
import https from 'https';

const outputWav = 'output.wav';
const url = "https://komelabo.sakura.ne.jp/kanonji/api/orion.php?type=Bus&id=jp.kagawa.kanonji.Bus.33"

const options = {
  method: 'GET'
}

const req = https.request(url,options, res => {
	console.log(`statusCode: ${res.statusCode}`)

	res.on('data', d => {
        let entities = JSON.parse(d);
        for(let entity of entities){
	        console.log(entity.title.value);
            // 合成するテキスト
            const text = entity.title.value;
            // textの内容を音声合成した後， 音声を再生するplayVoice関数を実行する
            synthesizeVoice(text, playVoice);
        }
	})
})


// Open JTalkで音声合成を行う関数
function synthesizeVoice(text, callback) {
  const command = `echo "${text}" | open_jtalk -x /usr/share/open_jtalk/dic -m /usr/share/hts-voice/nitech-jp-atr503-m001/nitech_jp_atr503_m001.htsvoice -ow ${outputWav}`;

  // コマンド実行
  exec(command, (error, stdout, stderr) => {
    if (error) {
      console.error(`音声合成中にエラーが発生しました: ${error.message}`);
      return;
    }
    if (stderr) {
      console.error(`エラーメッセージ: ${stderr}`);
      return;
    }
    console.log('音声合成が完了しました。');
    callback();
  });
}

// 音声を再生する関数
function playVoice() {
  exec(`aplay ${outputWav}`, (error, stdout, stderr) => {
    if (error) {
      console.error(`音声再生中にエラーが発生しました: ${error.message}`);
      return;
    }
    if (stderr) {
      console.error(`エラーメッセージ: ${stderr}`);
      return;
    }
    console.log('音声が再生されました。');
  });
}

req.on('error', error => {
	console.error(error)
})

req.end()
```

---

## スマートスピーカーにバスの行先を案内させよう
- プログラムを以下のように書き換える

#### 指定した経緯度の半径100m以内に進入したバスを取得するという条件に変更する
```js
const url = "https://komelabo.sakura.ne.jp/kanonji/api/orion.php?type=Bus&id=jp.kagawa.kanonji.Bus.33"

↓

const url = "https://komelabo.sakura.ne.jp/kanonji/api/orion.php?type=Bus&georel=near;maxDistance:100&geometry=point&coords=34.12771,133.66187";
```

#### バスの名称ではなく，そのバスの行先を案内する
```js
console.log(entity.title.value);
// 合成するテキスト
const text = entity.title.value;
// 出力する音声ファイル
synthesizeVoice(text, playVoice);

↓

let message = "";
if(entity.id=="jp.kagawa.kanonji.Bus.114"){
    message="このバスは五郷高室線バスです．大野原公民館へはこちらのバスをご利用ください．"
} else {
    continue;
}
console.log(message);
synthesizeVoice(message, playVoice);

```

#### プログラムを実行する
```bash=
node app.mjs 
```

---

#### プログラムを終了する
「Ctrl」キー + 「C」キー を同時に押す

---
