ボタンで棚を選択できるようにしました。選んでいる棚番号はLEDランプの点滅回数で表示します(フェーズ5F完了)。
# TanaRadio Pi フェーズ5F 詳細マニュアル
## 棚選びボタンとLED1を追加する
## 0. 今回の目標
フェーズ5Eでは、棚ファイルを指定してプレイリストを作り、mpvで再生できるようになりました。
フェーズ5Fでは、その「棚の切り替え」を物理ボタンで行えるようにします。
今回の動きは次の通りです。
```text
棚選びボタンを押す
↓
棚番号が1つ進む
↓
LED1が棚番号ぶん点滅する
↓
選ばれた棚ファイルから playlist.m3u を作る
↓
mpvをその棚で再生し直す
```
今回はまだ、再生モード選びボタンは追加しません。
再生モードは、5Eまでと同じく、
```text
過去3日分
古い順
```
を基本にします。
---
# 1. 今回使う棚構成
フェーズ5Fでは、フェーズ5Eで作った次の棚構成を使います。
```text
1: 声日記・生活 (voice) -> shelf_01_voice_life.txt
2: 技術・AI・ものづくり (tech) -> shelf_02_tech_ai_mono.txt
3: 教育・研究・知の実践 (edu) -> shelf_03_education_research.txt
4: 本・文化・物語 (books) -> shelf_04_books_culture.txt
5: TanaRadio (tana) -> shelf_05_tanaradio.txt
```
棚選びボタンを押すと、この順番で進みます。
```text
声日記・生活
↓
技術・AI・ものづくり
↓
教育・研究・知の実践
↓
本・文化・物語
↓
TanaRadio
↓
声日記・生活
```
LED1の点滅回数は、次の対応です。
```text
1回点滅:声日記・生活
2回点滅:技術・AI・ものづくり
3回点滅:教育・研究・知の実践
4回点滅:本・文化・物語
5回点滅:TanaRadio
```
---
# 2. 今回追加する部品
今回追加する部品は2つです。
```text
タクトスイッチ 1個
LED 1個
```
用途は次の通りです。
```text
タクトスイッチ:棚選びボタン
LED:棚番号表示LED
```
既存の3ボタンは、そのまま使います。
```text
再生/一時停止
戻る
次へ
```
つまり、フェーズ5F終了時点では、次の構成になります。
```text
ボタン1:再生/一時停止
ボタン2:戻る
ボタン3:次へ
ボタン4:棚選び
LED1:棚番号表示
```
---
# 3. GPIO番号
このマニュアルでは、次のGPIO番号を使います。
```text
再生/一時停止:GPIO17
戻る:GPIO27
次へ:GPIO22
棚選びボタン:GPIO23
棚番号LED:GPIO24
```
Raspberry Piの物理ピン番号では、次の対応です。
```text
GPIO17:物理ピン11
GPIO27:物理ピン13
GPIO22:物理ピン15
GPIO23:物理ピン16
GPIO24:物理ピン18
GND:物理ピン6, 9, 14, 20, 25 など
```
注意点です。
```text
GPIO番号と物理ピン番号は違います。
```
コードでは「GPIO番号」を使います。
たとえば、
```text
GPIO23
```
は、物理ピン番号では、
```text
16番ピン
```
です。
ここを取り違えると、ボタンを押しても無反応になります。静かな失敗です。電子工作ではいちばん地味に困るやつです。
---
# 4. 電源を切る
配線を触る前に、必ずRaspberry Piを終了します。
ターミナルで次を実行します。
```bash
sudo shutdown -h now
```
完全に電源が切れてから、USB-C電源を抜きます。
---
# 5. 棚選びボタンを配線する
棚選びボタンは、次のようにつなぎます。
```text
GPIO23 ─ 1kΩ抵抗 ─ タクトスイッチ ─ GND
```
この構成では、Python側で内部プルアップを使います。
そのため、動作は次のようになります。
```text
ボタンを押していない:HIGH
ボタンを押した:LOW
```
これまでのボタンと同じ考え方です。
---
# 6. 棚番号LEDを配線する
LEDは、次のようにつなぎます。
```text
GPIO24 ─ 330Ω抵抗 ─ LEDの長い足
LEDの短い足 ─ GND
```
LEDの足の目安です。
```text
長い足:プラス側
短い足:GND側
```
光らない場合は、まずLEDの向きを疑ってください。
LEDは、向きが逆だと基本的に光りません。
壊れたわけではなく、ただ黙っているだけです。
---
# 7. Raspberry Piを起動する
配線が終わったら、Raspberry Piの電源を入れます。
ターミナルを開き、作業フォルダに移動します。
```bash
cd ~/tanaradio5
```
現在地を確認します。
```bash
pwd
```
次のように表示されればOKです。
```text
/home/tana/tanaradio5
```
ユーザー名が違う場合は、そこだけ変わります。
---
# 8. 必要なパッケージを確認する
GPIOとmpvを使うため、念のため次を実行します。
```bash
sudo apt update
sudo apt install -y python3-gpiozero python3-lgpio mpv
```
すでに入っている場合は、そのままで大丈夫です。
---
# 9. 棚ファイルを確認する
今回使う棚ファイルがあるか確認します。
```bash
ls shelf_*.txt
```
次の5つが見えればOKです。
```text
shelf_01_voice_life.txt
shelf_02_tech_ai_mono.txt
shelf_03_education_research.txt
shelf_04_books_culture.txt
shelf_05_tanaradio.txt
```
それぞれの中身も確認します。
```bash
cat shelf_01_voice_life.txt
cat shelf_02_tech_ai_mono.txt
cat shelf_03_education_research.txt
cat shelf_04_books_culture.txt
cat shelf_05_tanaradio.txt
```
RSS URLが1行ずつ並んでいればOKです。
例です。
```text
https://listen.style/p/xxxx/rss
https://listen.style/p/yyyy/rss
https://listen.style/p/zzzz/rss
```
空の棚ファイルがある場合、その棚に切り替えたときにエラーになります。
テスト段階では、空の棚にも一時的に動作確認用のRSS URLを1本入れておくと安心です。
---
# 10. 5F用Pythonファイルを作る
新しいPythonファイルを作ります。
```bash
nano tanaradio_5f.py
```
次のコードをそのまま貼り付けます。
重要です。
最初の行は必ず、次で始めてください。
```text
#!/usr/bin/env python3
```
この前に、余計な文字を入れないでください。
```python
#!/usr/bin/env python3
import argparse
import json
import os
import signal
import socket
import subprocess
import time
import urllib.request
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from email.utils import parsedate_to_datetime
from pathlib import Path
from threading import Lock
from gpiozero import Button, LED
# =========================
# GPIO設定
# 5Dまでと違うGPIOを使っていた場合は、ここだけ直してください。
# BCM番号です。物理ピン番号ではありません。
# =========================
PLAY_GPIO = 17 # 再生/一時停止ボタン
PREV_GPIO = 27 # 戻るボタン
NEXT_GPIO = 22 # 次へボタン
SHELF_GPIO = 23 # 棚選びボタン(今回追加)
SHELF_LED_GPIO = 24 # 棚番号表示LED(今回追加)
# =========================
# ファイル設定
# =========================
BASE_DIR = Path(__file__).resolve().parent
PLAYLIST_FILE = BASE_DIR / "playlist.m3u"
STATE_FILE = BASE_DIR / "state_5f.json"
MPV_SOCKET = "/tmp/tanaradio_5f_mpv.sock"
# フェーズ5Fでは、まだ再生モードボタンは追加しません。
# 5Eまでと同じく「過去3日分・古い順」を基本にします。
DAYS_BACK = 3
# =========================
# 棚設定
# フェーズ5Eで作った棚構成に合わせています。
# =========================
SHELVES = [
{
"label": "声日記・生活",
"short": "voice",
"file": "shelf_01_voice_life.txt",
},
{
"label": "技術・AI・ものづくり",
"short": "tech",
"file": "shelf_02_tech_ai_mono.txt",
},
{
"label": "教育・研究・知の実践",
"short": "edu",
"file": "shelf_03_education_research.txt",
},
{
"label": "本・文化・物語",
"short": "books",
"file": "shelf_04_books_culture.txt",
},
{
"label": "TanaRadio",
"short": "tana",
"file": "shelf_05_tanaradio.txt",
},
]
@dataclass
class Episode:
title: str
audio_url: str
pub_date: datetime | None
feed_title: str
mpv_process: subprocess.Popen | None = None
current_shelf_index = 0
shelf_lock = Lock()
shelf_led: LED | None = None
def read_lines(path: Path) -> list[str]:
if not path.exists():
return []
lines = []
for raw_line in path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line:
continue
if line.startswith("#"):
continue
lines.append(line)
return lines
def load_state() -> int:
if not STATE_FILE.exists():
return 0
try:
data = json.loads(STATE_FILE.read_text(encoding="utf-8"))
index = int(data.get("shelf_index", 0))
if 0 <= index < len(SHELVES):
return index
except Exception:
pass
return 0
def save_state(index: int) -> None:
data = {
"shelf_index": index,
"shelf_label": SHELVES[index]["label"],
"shelf_short": SHELVES[index]["short"],
"shelf_file": SHELVES[index]["file"],
"saved_at": datetime.now().isoformat(timespec="seconds"),
}
STATE_FILE.write_text(
json.dumps(data, ensure_ascii=False, indent=2),
encoding="utf-8",
)
def download_rss(url: str) -> bytes:
request = urllib.request.Request(
url,
headers={"User-Agent": "TanaRadio-Pi/5F"},
)
with urllib.request.urlopen(request, timeout=25) as response:
return response.read()
def get_child_text(parent: ET.Element, tag_name: str) -> str:
element = parent.find(tag_name)
if element is not None and element.text:
return element.text.strip()
return ""
def find_audio_url(item: ET.Element) -> str | None:
# 一般的なRSS Podcastのenclosureタグ
enclosure = item.find("enclosure")
if enclosure is not None:
audio_url = enclosure.attrib.get("url")
if audio_url:
return audio_url
# media:content など、名前空間つきタグにも軽く対応
for child in item:
if child.tag.endswith("content"):
audio_url = child.attrib.get("url")
if audio_url:
return audio_url
return None
def parse_pub_date(text: str) -> datetime | None:
if not text:
return None
try:
dt = parsedate_to_datetime(text)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone()
except Exception:
return None
def parse_rss(rss_data: bytes, feed_url: str) -> list[Episode]:
root = ET.fromstring(rss_data)
channel = root.find("channel")
if channel is None:
raise ValueError("RSSのchannelが見つかりません。")
feed_title = get_child_text(channel, "title") or feed_url
episodes: list[Episode] = []
for item in channel.findall("item"):
audio_url = find_audio_url(item)
if not audio_url:
continue
title = get_child_text(item, "title") or "タイトルなし"
pub_date = parse_pub_date(get_child_text(item, "pubDate"))
episodes.append(
Episode(
title=title,
audio_url=audio_url,
pub_date=pub_date,
feed_title=feed_title,
)
)
return episodes
def collect_episodes_for_shelf(index: int) -> list[Episode]:
shelf = SHELVES[index]
shelf_path = BASE_DIR / shelf["file"]
feed_urls = read_lines(shelf_path)
if not feed_urls:
raise ValueError(f"{shelf_path.name} にRSS URLがありません。")
print()
print(f"棚 {index + 1}: {shelf['label']} ({shelf['short']})")
print(f"棚ファイル: {shelf_path.name}")
print(f"RSS数: {len(feed_urls)}")
all_episodes: list[Episode] = []
for feed_url in feed_urls:
print(f"RSS取得中: {feed_url}")
try:
rss_data = download_rss(feed_url)
episodes = parse_rss(rss_data, feed_url)
all_episodes.extend(episodes)
print(f" 取得: {len(episodes)} 件")
except Exception as e:
print(f" このRSSは読み飛ばしました: {e}")
if not all_episodes:
raise ValueError("音声エピソードを1件も取得できませんでした。")
now = datetime.now().astimezone()
cutoff = now - timedelta(days=DAYS_BACK)
filtered = [
ep for ep in all_episodes
if ep.pub_date is not None and ep.pub_date >= cutoff
]
if not filtered:
print(f"過去{DAYS_BACK}日分のエピソードがありません。")
print("動作確認しやすいように、今回は最新10件でプレイリストを作ります。")
filtered = sorted(
all_episodes,
key=lambda ep: ep.pub_date or datetime.min.replace(tzinfo=timezone.utc),
reverse=True,
)[:10]
filtered.sort(
key=lambda ep: ep.pub_date or datetime.min.replace(tzinfo=timezone.utc)
)
else:
filtered.sort(
key=lambda ep: ep.pub_date or datetime.min.replace(tzinfo=timezone.utc)
)
return filtered
def make_playlist(index: int) -> int:
episodes = collect_episodes_for_shelf(index)
lines = ["#EXTM3U"]
for ep in episodes:
date_text = ep.pub_date.isoformat(timespec="minutes") if ep.pub_date else "日付不明"
safe_title = ep.title.replace("\n", " ").strip()
safe_feed = ep.feed_title.replace("\n", " ").strip()
lines.append(f"# {date_text} / {safe_feed} / {safe_title}")
lines.append(ep.audio_url)
PLAYLIST_FILE.write_text(
"\n".join(lines) + "\n",
encoding="utf-8",
)
print(f"{PLAYLIST_FILE.name} を作成しました。")
print(f"音声URL数: {len(episodes)} 件")
return len(episodes)
def wait_for_mpv_socket(timeout: float = 5.0) -> bool:
start = time.time()
while time.time() - start < timeout:
if os.path.exists(MPV_SOCKET):
return True
time.sleep(0.1)
return False
def send_mpv_command(command: list[str | int | float]) -> None:
if not os.path.exists(MPV_SOCKET):
print("mpvのソケットが見つかりません。mpvがまだ起動していない可能性があります。")
return
payload = json.dumps({"command": command}) + "\n"
try:
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
client.connect(MPV_SOCKET)
client.sendall(payload.encode("utf-8"))
except Exception as e:
print(f"mpvへの命令送信に失敗しました: {e}")
def stop_mpv() -> None:
global mpv_process
if mpv_process is None:
return
if mpv_process.poll() is None:
try:
send_mpv_command(["quit"])
mpv_process.wait(timeout=3)
except Exception:
mpv_process.terminate()
try:
mpv_process.wait(timeout=3)
except Exception:
mpv_process.kill()
mpv_process = None
if os.path.exists(MPV_SOCKET):
try:
os.remove(MPV_SOCKET)
except OSError:
pass
def start_mpv() -> None:
global mpv_process
stop_mpv()
if os.path.exists(MPV_SOCKET):
os.remove(MPV_SOCKET)
print("mpvを起動します。")
mpv_process = subprocess.Popen(
[
"mpv",
"--input-ipc-server=" + MPV_SOCKET,
"--force-window=no",
str(PLAYLIST_FILE),
]
)
if not wait_for_mpv_socket():
print("mpvの操作ソケットを確認できませんでした。")
print("再生自体は始まっている可能性があります。")
def blink_shelf_number(index: int) -> None:
if shelf_led is None:
return
count = index + 1
print(f"LED点滅: {count}回")
for _ in range(count):
shelf_led.on()
time.sleep(0.18)
shelf_led.off()
time.sleep(0.18)
time.sleep(0.25)
def blink_error() -> None:
if shelf_led is None:
return
for _ in range(6):
shelf_led.on()
time.sleep(0.07)
shelf_led.off()
time.sleep(0.07)
def select_shelf(index: int, restart_player: bool = True) -> bool:
global current_shelf_index
with shelf_lock:
try:
make_playlist(index)
current_shelf_index = index
save_state(index)
blink_shelf_number(index)
if restart_player:
start_mpv()
print(f"現在の棚: {index + 1} / {SHELVES[index]['label']} ({SHELVES[index]['short']})")
return True
except Exception as e:
print("棚の切り替えに失敗しました。")
print(e)
blink_error()
return False
def next_shelf() -> None:
next_index = (current_shelf_index + 1) % len(SHELVES)
print()
print(f"棚ボタン: {current_shelf_index + 1} → {next_index + 1}")
print(f"次の棚: {SHELVES[next_index]['label']} ({SHELVES[next_index]['short']})")
select_shelf(next_index, restart_player=True)
def on_play() -> None:
print("再生/一時停止")
send_mpv_command(["cycle", "pause"])
def on_prev() -> None:
print("戻る")
send_mpv_command(["playlist-prev"])
def on_next() -> None:
print("次へ")
send_mpv_command(["playlist-next"])
def list_shelves() -> None:
print("棚一覧")
for i, shelf in enumerate(SHELVES, start=1):
path = BASE_DIR / shelf["file"]
urls = read_lines(path)
print(
f"{i}: {shelf['label']} ({shelf['short']}) "
f"-> {path.name} RSS数: {len(urls)}"
)
def test_led() -> None:
global shelf_led
shelf_led = LED(SHELF_LED_GPIO)
try:
for index, shelf in enumerate(SHELVES):
print(f"棚{index + 1}: {shelf['label']} ({shelf['short']}) の点滅テスト")
blink_shelf_number(index)
time.sleep(0.5)
finally:
shelf_led.off()
shelf_led.close()
shelf_led = None
def main() -> None:
global current_shelf_index, shelf_led
parser = argparse.ArgumentParser(description="TanaRadio Pi フェーズ5F")
parser.add_argument("--list", action="store_true", help="棚一覧を表示する")
parser.add_argument("--test-led", action="store_true", help="LEDの点滅をテストする")
args = parser.parse_args()
if args.list:
list_shelves()
return
if args.test_led:
test_led()
return
current_shelf_index = load_state()
print("TanaRadio Pi フェーズ5Fを起動します。")
print()
list_shelves()
print()
print(
f"前回の棚: {current_shelf_index + 1} / "
f"{SHELVES[current_shelf_index]['label']} "
f"({SHELVES[current_shelf_index]['short']})"
)
shelf_led = LED(SHELF_LED_GPIO)
play_button = Button(PLAY_GPIO, pull_up=True, bounce_time=0.08)
prev_button = Button(PREV_GPIO, pull_up=True, bounce_time=0.08)
next_button = Button(NEXT_GPIO, pull_up=True, bounce_time=0.08)
shelf_button = Button(SHELF_GPIO, pull_up=True, bounce_time=0.12)
play_button.when_pressed = on_play
prev_button.when_pressed = on_prev
next_button.when_pressed = on_next
shelf_button.when_pressed = next_shelf
try:
ok = select_shelf(current_shelf_index, restart_player=True)
if not ok:
print("最初の棚を作れませんでした。棚ファイルを確認してください。")
return
print()
print("操作できます。")
print("再生/一時停止、戻る、次へ、棚選びボタンを押してください。")
print("終了するには Ctrl + C を押します。")
signal.pause()
except KeyboardInterrupt:
print()
print("終了します。")
finally:
stop_mpv()
if shelf_led is not None:
shelf_led.off()
shelf_led.close()
play_button.close()
prev_button.close()
next_button.close()
shelf_button.close()
if __name__ == "__main__":
main()
```
保存します。
```text
Ctrl + O
Enter
Ctrl + X
```
---
# 11. 実行権限を付ける
次を実行します。
```bash
chmod +x tanaradio_5f.py
```
ただし、実行は `python3` で行うのが安全です。
```bash
python3 tanaradio_5f.py
```
以前のように、Pythonファイルをシェルスクリプトとして読んでしまう事故を避けるためです。
---
# 12. 棚一覧を確認する
まず、棚ファイルが正しく認識されているか確認します。
```bash
python3 tanaradio_5f.py --list
```
期待される表示例です。
```text
棚一覧
1: 声日記・生活 (voice) -> shelf_01_voice_life.txt RSS数: 8
2: 技術・AI・ものづくり (tech) -> shelf_02_tech_ai_mono.txt RSS数: 6
3: 教育・研究・知の実践 (edu) -> shelf_03_education_research.txt RSS数: 5
4: 本・文化・物語 (books) -> shelf_04_books_culture.txt RSS数: 4
5: TanaRadio (tana) -> shelf_05_tanaradio.txt RSS数: 1
```
RSS数は実際のファイル内容によって変わります。
ここで重要なのは、次の対応が正しく出ることです。
```text
1: 声日記・生活 -> shelf_01_voice_life.txt
2: 技術・AI・ものづくり -> shelf_02_tech_ai_mono.txt
3: 教育・研究・知の実践 -> shelf_03_education_research.txt
4: 本・文化・物語 -> shelf_04_books_culture.txt
5: TanaRadio -> shelf_05_tanaradio.txt
```
---
# 13. LEDだけテストする
いきなり再生まで行かず、まずLEDの点滅を確認します。
```bash
python3 tanaradio_5f.py --test-led
```
期待される動きです。
```text
棚1:LEDが1回点滅
棚2:LEDが2回点滅
棚3:LEDが3回点滅
棚4:LEDが4回点滅
棚5:LEDが5回点滅
```
表示例です。
```text
棚1: 声日記・生活 (voice) の点滅テスト
LED点滅: 1回
棚2: 技術・AI・ものづくり (tech) の点滅テスト
LED点滅: 2回
棚3: 教育・研究・知の実践 (edu) の点滅テスト
LED点滅: 3回
棚4: 本・文化・物語 (books) の点滅テスト
LED点滅: 4回
棚5: TanaRadio (tana) の点滅テスト
LED点滅: 5回
```
LEDが光らない場合は、次を確認します。
```text
LEDの向きが逆ではないか
GPIO24ではなく別のピンにつないでいないか
GNDにつながっているか
抵抗を入れているか
```
---
# 14. 5Fを起動する
いよいよ5Fを起動します。
```bash
python3 tanaradio_5f.py
```
うまくいくと、次のような表示になります。
```text
TanaRadio Pi フェーズ5Fを起動します。
棚一覧
1: 声日記・生活 (voice) -> shelf_01_voice_life.txt RSS数: 8
2: 技術・AI・ものづくり (tech) -> shelf_02_tech_ai_mono.txt RSS数: 6
3: 教育・研究・知の実践 (edu) -> shelf_03_education_research.txt RSS数: 5
4: 本・文化・物語 (books) -> shelf_04_books_culture.txt RSS数: 4
5: TanaRadio (tana) -> shelf_05_tanaradio.txt RSS数: 1
前回の棚: 1 / 声日記・生活 (voice)
棚 1: 声日記・生活 (voice)
棚ファイル: shelf_01_voice_life.txt
RSS数: 8
RSS取得中: ...
playlist.m3u を作成しました。
音声URL数: 10 件
LED点滅: 1回
mpvを起動します。
現在の棚: 1 / 声日記・生活 (voice)
操作できます。
再生/一時停止、戻る、次へ、棚選びボタンを押してください。
終了するには Ctrl + C を押します。
```
音が出れば、まず成功です。
---
# 15. 既存3ボタンを確認する
次のボタンを押します。
```text
再生/一時停止
戻る
次へ
```
ターミナルに次のように表示されれば、ボタン入力は読めています。
```text
再生/一時停止
戻る
次へ
```
音声もそれに合わせて動けばOKです。
---
# 16. 棚選びボタンを押す
棚選びボタンを1回押します。
棚1から棚2に進む場合、期待される動きは次の通りです。
```text
棚1:声日記・生活
↓
棚2:技術・AI・ものづくり
```
ターミナルには、たとえば次のように表示されます。
```text
棚ボタン: 1 → 2
次の棚: 技術・AI・ものづくり (tech)
棚 2: 技術・AI・ものづくり (tech)
棚ファイル: shelf_02_tech_ai_mono.txt
RSS数: 6
RSS取得中: ...
playlist.m3u を作成しました。
音声URL数: 12 件
LED点滅: 2回
mpvを起動します。
現在の棚: 2 / 技術・AI・ものづくり (tech)
```
さらに押すと、次のように進みます。
```text
2 → 3:教育・研究・知の実践
3 → 4:本・文化・物語
4 → 5:TanaRadio
5 → 1:声日記・生活
```
LED点滅の対応は次の通りです。
```text
1回点滅:声日記・生活
2回点滅:技術・AI・ものづくり
3回点滅:教育・研究・知の実践
4回点滅:本・文化・物語
5回点滅:TanaRadio
```
---
# 17. 前回の棚が保存されるか確認する
一度終了します。
```text
Ctrl + C
```
もう一度起動します。
```bash
python3 tanaradio_5f.py
```
前回最後に選んだ棚から始まれば成功です。
棚の状態は、次のファイルに保存されます。
```text
state_5f.json
```
中身を確認するには、次を実行します。
```bash
cat state_5f.json
```
表示例です。
```json
{
"shelf_index": 1,
"shelf_label": "技術・AI・ものづくり",
"shelf_short": "tech",
"shelf_file": "shelf_02_tech_ai_mono.txt",
"saved_at": "2026-07-06T09:30:00"
}
```
ここで `shelf_index` は0始まりです。
つまり、
```text
shelf_index 0:棚1
shelf_index 1:棚2
shelf_index 2:棚3
shelf_index 3:棚4
shelf_index 4:棚5
```
です。
---
# 18. 成功条件
フェーズ5Fの成功条件は、次の4つです。
```text
1. 棚選びボタンを押すと、棚番号が進む
2. LED1が棚番号ぶん点滅する
3. 選ばれた棚ファイルに応じて playlist.m3u が作り直される
4. mpvが新しい棚で再生し直される
```
棚番号の対応は次の通りです。
```text
1:声日記・生活
2:技術・AI・ものづくり
3:教育・研究・知の実践
4:本・文化・物語
5:TanaRadio
```
LED点滅の対応は次の通りです。
```text
1回点滅:声日記・生活
2回点滅:技術・AI・ものづくり
3回点滅:教育・研究・知の実践
4回点滅:本・文化・物語
5回点滅:TanaRadio
```
---
# 20. 今回はここで止めてよい
フェーズ5Fでは、ここまでで十分です。
今回できるようになることは、次です。
```text
物理ボタンで棚を選べる
LEDの点滅で棚番号を確認できる
棚ごとのRSSリストからプレイリストを作れる
選んだ棚をmpvで再生し直せる
```
これはかなり大きな進歩です。
フェーズ5Eまでは、棚はまだ「コマンドで指定するもの」でした。
フェーズ5Fで、棚は「手元のボタンで選ぶもの」になります。
ここから一気にラジオ感が出ます。
次のフェーズ5Gでは、5つ目のボタンとLED2を追加して、再生モードを選べるようにします。
候補は、まず次の3つで十分です。
```text
1:最新順
2:今日
3:3日前から古い順
```
「7日前から古い順」や「未聴っぽいもの」は、そのあとで大丈夫です。
ここで焦らないのが勝ち筋です。
#声日記 #TanaRadioPi
LISTENで開く