電子辞書 SHARP Brain PW-SH6 を Nerves/Elixir で KIOSK 端末にした(有線/WiFi/BLE、USB・BT オーディオ、RTC 同期まで)
はじめに
注意: この記事は開発途中の試行錯誤の記録です。手順・数値は 2026-09 時点の私の環境での実測で、
再現性や動作を保証するものではありません。電子辞書の改造は自己責任で、失敗すると起動しなくなる可能性が
あります(本体の eMMC には触らず、microSD からの起動のみで作業しています)。
中古で手に入る SHARP の電子辞書 Brain PW-SH6(2017 年頃のモデル)は、中身が NXP i.MX283(ARM926EJ-S、454MHz、RAM 128MiB) の小さな Linux 機として遊べることが brain-hackers コミュニティの成果(Brainux)で知られています。
この記事は、その Brain を Nerves(Elixir の組み込み Linux フレームワーク)で動く KIOSK 端末にした約 2 週間の記録です。最終的にこうなりました。
- 5.5 インチ 854×480 の LCD に LovyanGFX(NIF)で日本語 UI、タッチ・キーボード・電池・時計表示
- USB ホスト化して 有線 LAN + WiFi + BLE を起動時に自動接続(3 系統同時)
- BLE で SwitchBot 温湿度計/気圧/開閉/人感 を受信してライブ表示
- USB オーディオでタッチ音・起動チャイム・時報と A〜Z の読み上げ(Open JTalk で事前合成)
- Bluetooth スピーカー(A2DP)へ BlueZ + BlueALSA で出力
- 本体内蔵 RTC と Linux 時計の双方向同期(電池が切れても LAN に繋げば Brain の時計が直る)
- SD カードを焼くだけで全機能が上がるマスタイメージ、機種(PW-SH1/SH6…)は DTB から自動認識
公開しているもの:
- Nerves システム(Buildroot 定義・起動スクリプト・SD 作成手順):
https://github.com/piyopiyoex/nerves_system_brain -
動作デモ(KIOSK アプリの実行イメージと使い方):
https://github.com/piyopiyoex/nerves_system_brain/tree/main/examples/hello_kiosk
KIOSK アプリ本体(Elixir ソース)は現時点では公開していません。本文のコードはそこからの抜粋です。
ARMv5・soft-float・128MiB という、Nerves 公式ターゲット(RPi 系は ARMv6 以上)より一段低いところで動かしているので、同じような「非公式ボードで Nerves」を考えている人の参考になれば。
1. ハードウェアと制約
| 項目 | 内容 |
|---|---|
| SoC | NXP i.MX283、ARM926EJ-S(ARMv5TEJ)454MHz、FPU なし(soft-float) |
| RAM / ストレージ | 128MiB / 内蔵 eMMC(触らない)+ microSD から起動 |
| 表示 | 5.5" 854×480、ILI9805 系パネル、/dev/fb0 RGB565 |
| 入力 | 抵抗膜タッチ + フルキーボード(evdev) |
| USB | micro-B(OTG)。USB ホスト化には OTG ケーブル + セルフパワーハブが必要(バス給電では不安定) |
| 音 | 内蔵スピーカーあり。ただし Linux から鳴らす経路は未解明(後述) |
| 時計 | i.MX28 内蔵 RTC(バックアップ電池なし → 主電池が切れるとリセット) |
| 起動 | Brain 純正ファームから 機種名の実行ファイル(例 edsh6exe.bin)= U-Boot を起こし、SD の zImage + 機種別 DTB を読む(brain-hackers の仕組み) |
Nerves 公式は最小でも RPi Zero(ARMv6、512MiB)。Brain は ARMv5 で半分以下の RAM なので、以下は諦めています。
- 実機上での Erlang/Elixir コンパイル、
mix実行 → 全部母艦(x86_64)でクロスビルドし beam を送る - 実機上での TTS・音声認識・重い画像処理 → 音声は事前合成、描画は C++(NIF)
- fwup による OTA → SD の rootfs を直接更新する運用(SD 可搬)
1-2. 使った部品(Amazon で購入、実機で動作確認済み)
型番・ASIN は手元の購入履歴で補完してください。「確認内容」は実際に Brain 上で動いた範囲です。
| 分類 | 製品(チップ) | 確認内容 | 備考 |
|---|---|---|---|
| 本体 | SHARP Brain PW-SH6(中古)。PW-SH1 でも同 SD で起動確認 | KIOSK 全機能 | 機種は DTB から自動認識 |
| 起動メディア | microSD 4GB 以上(高耐久品推奨) | Nerves rootfs + brain-hackers 起動チェーン | 取り違えでデバッグが迷走した経験あり。ラベルを貼る |
| USB ホスト化 | USB2.0 OTG ケーブル(micro-B オス → A メス) | ホストモード成立 | ケーブルによって給電挙動が変わる。安物で相性あり |
| USB ホスト化 | セルフパワー USB2.0 ハブ(AC アダプタ付き) | 有線 LAN + WiFi + BLE(または BT)+ USB オーディオの同時接続 | バスパワーでは不安定(ドングル追加で WiFi が落ちた)。ハブ経由で本体充電も可 |
| 有線 LAN | USB Gigabit LAN アダプタ(ASIX AX88179) | DHCP・NTP・SSH、常用経路 |
ax88179_178a ドライバ |
| WiFi | UWF-1(Atheros AR9271、ath9k_htc) | WPA2 接続・DHCP、有線と同時稼働 | 2.4GHz のみ。ハブの Full-Speed 交渉で USB ハングした個体あり(ポート構成で回避) |
| BLE / Bluetooth | エレコム LBT-UAN06C2/EC(Realtek RTL8761BU、BT 5.3) | SwitchBot BLE 受信 / A2DP ソース(BlueZ+BlueALSA) | 同時には使えない(hci0 排他)。両立にはもう 1 本 |
| USB オーディオ | USB サウンドアダプタ(GeneralPlus 1b3f:2008、UAC) | tinyalsa で再生(タッチ音・チャイム・時報・A–Z)、マイク入力あり(未使用) | S16_LE / 2ch 固定。1,000 円前後 |
| BT スピーカー | ZoeeTree S1+ | A2DP でペアリング・自動再接続・再生 | ストリーム開始時に約 1 秒の立ち上がり無音あり(記事本文の対策) |
| BT スピーカー | LenoPlay Bluetooth スピーカー(IPX7、AUX 入力あり) | 未検証(受信側スピーカー) | AUX 経由なら USB オーディオの出力先として使える想定 |
| センサ | SwitchBot 温湿度計(dev_type 0x54)/ 温湿度計プラス系(0x69)/ 防水温湿度計(0x77) | BLE 広告受信(温度・湿度・電池・RSSI) | 最大 3 台表示 |
| センサ | SwitchBot 開閉センサ(0x64)/ 人感センサ(0x73) | BLE 広告受信(開閉・人感・明るさ・電池) | |
| (参考) | 気圧計 piyo2 | BLE 受信 | 自作機(Amazon 品ではない) |
Amazon 以外に用意したもの: 母艦(x86_64 Linux)、SD カードリーダ。ロジアナ・オシロは使っていません(使えていれば内蔵音声の調査はもう少し進んだはず)。
2. 全体構成
母艦 (AlmaLinux 10, x86_64)
├─ nerves_system_brain … Buildroot 2026.05.2 + nerves_system_br v1.34.3、Bootlin armv5-eabi glibc ツールチェーン
│ rootfs_overlay/ (erlinit.config, 起動スクリプト, ファームウェア, dbus/bluez 設定)
│ nerves_defconfig (wpa_supplicant, iw, tinyalsa, dbus, bluez5_utils, bluez-alsa, alsa-utils)
├─ hello_kiosk_brain … Elixir アプリ(OTP 29 / Elixir 1.20)+ LovyanGFX NIF(C++)
└─ カーネル/U-Boot/DTB … brain-hackers の buildbrain 成果物(Linux 6.1.70)を流用、BT/RTL 有効の再ビルド版
SD カード
p1 (FAT) zImage, imx28-pwsh1..7.dtb, edsh6exe.bin(U-Boot) など brain-hackers の起動チェーン
p2 (ext4) Nerves rootfs(erlinit → /usr/bin/enable_net → Erlang VM)
ポイントは erlinit の --pre-run-exec に自前の起動スクリプト enable_net を挿し、Erlang が上がる前に RTC→時計・有線 LAN・WiFi・(設定があれば)BT スタックをバックグラウンドで立ち上げていること。Nerves の scrub-target.sh は /etc/init.d を消すので、init スクリプトの類は全部ここに集約しています。
3. 画面: LovyanGFX を NIF で
Elixir から /dev/fb0 に直接描くと ARMv5 では日本語フォント描画が重すぎるので、LovyanGFX(ESP32 でおなじみの描画ライブラリ、Linux fb バックエンドあり)を C++ の NIF にして、Elixir 側は「描画コマンド列」を 1 回の render/1 で渡す方式にしました。
[
Draw.rect(0, 0, @w, 40, @panel),
Draw.text(12, 20, :ml, :jp24, @fg, model_name()), # "PW-SH6"
Draw.text(@title_x, 20, :ml, :jp24, @accent, title),
Draw.text(div(@w, 2), 12, :mc, :jp16, ccol, clock),
...
] |> Native.render()
ヘッダは毎秒描き直していますが、コマンド列を組み立てるのは Elixir、ラスタライズは NIF なので 128MiB/454MHz でも余裕です。「デモ」タブのスプライト 50 個アニメーションは NIF 側スレッドで 11~21fps。

4. ネットワーク: USB ホスト化して有線 + WiFi + BLE
Brain の USB は OTG。OTG ケーブル + セルフパワーハブに、有線 LAN(AX88179)・WiFi(UWF-1 = AR9271/ath9k_htc)・BLE(ELECOM LBT-UAN06C2 = RTL8761BU)を挿しています。
-
有線: udhcpc + 自前
wired.script(busybox の default.script はrouteapplet が無くてハングした) -
WiFi: wpa_supplicant(nl80211)+ udhcpc。
/etc/wpa_supplicant.confの実 PSK は git に入れず、Nerves の scrub 後に post-build で母艦ローカルから入れる -
経路: 有線 metric 0 / WiFi metric 20 で両方張り、NTP は
ntpd -q
BLE は BlueZ を使わず、Erlang の :socket で AF_BLUETOOTH の HCI USER チャネルを直接開いて HCI コマンドを叩いています。
{:ok, sock} = :socket.open(31, :raw, 1) # AF_BLUETOOTH, BTPROTO_HCI
:socket.bind(sock, %{family: 31, addr: <<0::16, 1::16>>}) # hci0, HCI_CHANNEL_USER
hci(sock, 0x03, 0x0c, <<>>) # Reset
hci(sock, 0x01, 0x0c, all_ones) # Set Event Mask ← これを忘れると広告が一切来ない
hci(sock, 0x01, 0x20, all_ones) # LE Set Event Mask
hci(sock, 0x0b, 0x20, <<1, 0x10, 0, 0x10, 0, 0, 0>>) # アクティブスキャン
hci(sock, 0x0c, 0x20, <<1, 0>>) # Scan Enable
SwitchBot の広告(Service Data 0xFD3D + Manufacturer 0x0969)をデコードして、温湿度計 3 台・気圧計・開閉・人感をカードで表示:

5. 音: 内蔵は諦めて USB、その先に Bluetooth
5-1. 内蔵スピーカーは鳴らせなかった
DTS には SGTL5000 コーデックがあることになっていますが実機の I²C にはおらず(EVK 由来の宣言)、代わりに 0x1a に正体不明の I²C デバイスがいます。PWM ブザーも、ドライバ(pwm-beeper)まで完璧に動くのに基板配線が届いていないらしく無音。ロジアナも回路図も無いので、ここは保留にして USB オーディオに切り替えました(調査記録はリポジトリの docs/ に)。
5-2. USB オーディオ + tinyalsa
GeneralPlus の 1,000 円くらいの USB サウンドアダプタ(UAC、S16_LE/2ch 固定)を tinyalsa(tinyplay)で鳴らします。alsa-lib より小さく設定ファイル不要。モノラル WAV は 2ch 固定のアダプタで開けないので、Elixir 側で L/R 複製してステレオ化しています。
音声は実機で TTS を回さず、Open JTalk(Mei ボイス)で母艦で事前合成した WAV を連結再生:
5-3. Bluetooth スピーカー(A2DP)へ
買ってきた BT スピーカーが受信専用(当然)だったので、BLE 用の RTL8761BU ドングルで BlueZ 5.79 + BlueALSA 4.3.1 の A2DP ソースを立てました。Buildroot に dbus, bluez5_utils, bluez-alsa, alsa-utils を追加し、erlinit 配下で
dbus-daemon --config-file=/etc/dbus-1/brain-system.conf # root で動く最小 system bus
bluetoothd -n --noplugin=sap,hostname,autopair # main.conf: AutoEnable, bredr
bluealsa -p a2dp-source --keep-alive=120
を起動。ペアリング〜接続は Elixir から bluetoothctl を非対話で叩き(--timeout 8 scan on → pair → trust → connect)、bluealsa-cli list-pcms に A2DP PCM が出たら再生可、と判定します。再生は aplay -D bluealsa:DEV=<MAC>,PROFILE=a2dp。
ここで一番ハマったのがタッチ音(70ms)です。A2DP はストリーム開始時にスピーカー側で約 1 秒の立ち上がり無音があり、1 音ごとに aplay を起動する方式では短い音が丸ごと消えます(0.5 秒間隔で 8 連打 → 6 回しか聞こえない)。解決策は aplay を常駐ポートにして「ほぼ無音」を流し続け、効果音をその流れに混ぜること:
# 80ms ごとに、壁時計 + 260ms 先まで PCM を書く。待ち行列に音があればそれ、無ければ keep-alive 信号
def handle_info(:bt_tick, %{bt: bt, lead_ms: lead} = state) do
need_ms = now_ms() - bt.t0 + lead - bt.written_ms
...
{chunk, rest} = take_pcm(bt.pending, w_ms * @bt_bytes_per_ms)
Port.command(bt.port, chunk)
「ほぼ無音」はデジタル 0 ではなく 25Hz・-49dBFS の正弦波 + ±2LSB ディザ(スピーカーの無音検出ミュートを外す)。デモ画面(NIF が CPU を占有)中は先行量を 1 秒に増やしてアンダーランを防ぎ、aplay には --start-delay を明示(既定の「バッファ満杯まで待つ」だと 1.3 秒の遅延が常時乗る)。CPU は常時 25% ほど食いますが、タッチから 0.5 秒程度で確実に鳴るようになりました。
同じドングルで BLE 受信と BT 音声は排他(bluetoothd が hci0 を掴むと USER チャネルは取れない)なので、機体ごとに /root/bt_speaker.conf の有無でモードを切り替えています。両立したければドングル 2 本。
6. 時計: Brain の RTC は「2000 年起点の JST」
Linux 側は起動すると 1970 年で、NTP が来るまで時刻が無い。一方 Brain 純正ファームの時計は主電池が持つ限り保持される……ので調べたら、i.MX28 内蔵 RTC(HW_RTC_SECONDS @0x80056030)を、純正ファームが「2000-01-01 起点の秒・ローカル時刻(JST)」として使っていることが分かりました。Linux の rtc-stmp3xxx ドライバは 1970 起点 UTC 前提なので、brain-hackers の DTS では RTC ノードが disabled になっています(そのまま有効化すると 30 年ずれる)。
ドライバは触らず、ユーザ空間でレジスタを直接読み書き:
- 起動 5 秒(
enable_net):devmem 0x80056030→date -s(オフラインでも即・正時刻) - Erlang(
BrainClock): NTP 後に Linux → RTC を書き戻す(NEW_REGS が 0 のときだけ、PERSISTENT レジスタは触らない)
これで Brain 純正ファームに戻しても時計が合っている状態になりました(電池が切れても LAN に繋げば直る)。
7. ハマったこと(抜粋)
| 事象 | 原因 | 対処 |
|---|---|---|
| 起動後 30 秒以上タッチが効かない |
:ssh の user_passwords が PBKDF2 を回し、ARMv5 で 35 秒 CPU を占有 |
pwdfun に置き換え |
busybox で chmod/head/awk/route/wc が無い |
Nerves のシェルは最小 applet | 絶対パス・File.chmod!・自前 udhcpc スクリプト |
| BLE 広告が 1 件も来ない | Set Event Mask / LE Set Event Mask を送っていなかった | 上記 §4 |
| BLE ドングルを挿すと WiFi が切れる | OTG ケーブル/ハブの給電 | セルフパワーハブ + 別ケーブル |
| UWF-1(AR9271)が USB ハング | ハブ経由で Full-Speed 接続になり HS 前提のディスクリプタと不整合 | ポート・ハブ構成を変更 |
| tinyalsa でモノラル WAV が無音 | UAC アダプタが 2ch 固定 | 再生時にステレオ化 |
| dbus-daemon が起動しない | 設定 XML のコメント内に --(--config-file と書いていた) |
コメント修正。ソケットも /run/dbus(Nerves は /var/run→/run の symlink なし) |
| buildroot が host-libxcrypt で止まる | 母艦 perl に core の open.pm が無い(最小インストール) |
dnf install perl-open |
| 実機で 500KB のバイナリを Elixir の binary comprehension で舐めたら固まった | eval_bits がインタプリタ実行 | 母艦へ持ち帰って strings/cmp
|
| 電源 OFF ボタンで再起動してしまう | 5V 給電中は i.MX28 のシリコン仕様で電源断維持不可 | 仕様として案内表示 |
| バックライトを消したい | 制御線が LCD 電源と一体、GPIO を触るとパネルが白飛び | 諦め(純正は 5 段階調光できるので経路はあるはず。調査中) |
8. 開発サイクル
SD を抜き差しせずに回せるのが快適でした。
MIX_ENV=prod mix compile # 母艦でクロス(beam は機種非依存)
sftp user@192.168.1.42 <<< "put _build/prod/lib/hello_kiosk_brain/ebin/Elixir.HelloKioskBrain.Kiosk.beam /srv/erlang/lib/hello_kiosk_brain-0.1.0/ebin/"
ssh user@192.168.1.42 ':code.purge(HelloKioskBrain.Kiosk); :code.load_file(HelloKioskBrain.Kiosk)'
./scripts/screenshot.sh # /dev/fb0 をダンプして PNG に
SSH は「Elixir 式をそのまま評価する」exec 専用にしていて、ssh user@brain 'HelloKioskBrain.BrainClock.read_rtc()' のように使えます(対話シェルは持たない。日本語や改行を式に含めるとパーサに蹴られるので ASCII で)。
システム側の変更(Buildroot)は rootfs_overlay/ と nerves_defconfig に集約し、素の buildroot ビルドから同じ rootfs が再現できることを確認しています。稼働中の機体には sftp で beam・スクリプト・ライブラリを送って /sbin/reboot(erlinit は --run-on-exit /bin/sh なので VM 停止では再起動しない)。
9. まとめ
- ARMv5・128MiB でも Nerves は動く。ただし「実機で重いことはしない」(クロスビルド、事前合成、描画は NIF)を徹底
- 起動時処理は erlinit
--pre-run-execの自前スクリプトに集約し、必ずバックグラウンド化 - USB ホスト化で有線/WiFi/BLE、USB オーディオ、BT スピーカーと「外付け」で機能を足していける
- 純正ファームの流儀(RTC の起点、機種別起動ファイル)を読み解くと、Nerves 側から素直に共存できる
- できなかったこと(内蔵音声・ブザー・バックライト)は「なぜできないか」まで docs/ に残した
動かしてみたい方は nerves_system_brain の examples/hello_kiosk から。同じ Brain を持っている方、あるいは別の「Nerves 非対応ボード」に挑む方の参考になれば嬉しいです。
参考
- brain-hackers — Brainux(カーネル・U-Boot・DTB・buildbrain)
- Nerves Project / nerves_system_br
- LovyanGFX
- BlueALSA
- Open JTalk / Mei ボイス(MMDAgent、CC BY 3.0)

