0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Raspberry Pi 2 の CTI サーバーを Radxa Zero 3E に移行した話

0
Last updated at Posted at 2026-09-06

はじめに

自作の CTI サーバー(電話の着信を検知して、顧客情報をクライアントに表示する仕組み)を、10年以上 Raspberry Pi 2 上で動かしてきた。ネットワークのミラーポートで NTT ひかり電話の SIP パケットを監視し、着信番号を抽出して WebSocket でクライアントに配信する、という構成だ。

問題は Raspberry Pi 2 がとっくに手に入らなくなったこと。引き合いが来るたびに中古を探す羽目になっていた。そこで後継ボードを Radxa Zero 3E に定め、サーバー側を作り直した。その記録を残しておく。

結論から言うと、開封から「実機で動いて配れるイメージができる」まで、実質3日ほどだった。


なぜ Radxa Zero 3E か

候補を検討して、最終的に Zero 3E にした理由は主に3つ。

  • 有線 LAN が内蔵されている。 ミラーポート監視には必須。
  • 無線モジュールを持たない。 技適の心配が要らない(顧客に納める機器として地味に大きい)。
  • 供給が保証されている。 メーカーが 2030 年まで供給を明言していた。Pi 2 の二の舞を避けたい。
    一方で難点もあった。日本に代理店がなく、専用ケースも既製品がほぼ無い。 今回は 2GB 版を Arace(Radxa 公式直販)で 1個 6,600 円(別途送料)で入手した。AliExpress でも探したが、割高で怪しいものが多く、避けた。日本の Amazon 等では転売で 15,000 円ほどの出品もある。公式直販から買うのが結局いちばん安く確実だった。

RK3566(Cortex-A55 クアッド、arm64)、2GB RAM、microSD 起動。Raspberry Pi Zero とほぼ同じ 65×30mm のサイズ感で、RJ45 が短辺から生え、USB-C が2つ長辺に並ぶ。


移行前にやったこと:正本化とテストデータの採取

いきなり新ボードにコードを書き始めず、先に足元を固めた。これが後で効いた。

  • 旧サーバーのソースを正本化した。 実は12年間ほとんど版管理されておらず、「実機にしかない改修」と「リポジトリにしかないコード」が食い違っていた。実機の内容を正として Git に取り込み直した。

  • 稼働中の実機から、テスト用のフィクスチャを採った。 実際に電話をかけて、

    1. tcpdump のテキスト出力(パーサへの入力)
    2. 旧サーバーが WebSocket に流した JSON(期待値)
    3. DB に記録された履歴

    の3層を、同じ着信について記録した。着信(登録済み/未登録)、非通知、発信抑制など、いくつかのパターンで。個人情報はマスクしてから保存した。

このフィクスチャがあると、新実装が「旧サーバーと同じ入力に同じ出力を返すか」を機械的に検証できる。移行先を書きながら、旧版との等価性をテストで担保できたのが大きい。 稼働中の旧機が生きているうちにしか採れないデータなので、先にやっておいて正解だった。


何で書いたか:Node.js、依存は最小

再実装は Node.js(LTS)で。旧サーバーもロジックは JavaScript だったので、一番の資産である SIP 解析ロジックをほぼそのまま引き継げる、という理由が大きい。

ただし、旧サーバーが動かなくなった直接の原因は npm 依存の腐敗だった(Express 3、古い WebSocket ライブラリ、ネイティブ拡張の SQLite などが軒並み EOL)。同じ轍を踏まないよう、依存を極限まで減らした。

  • HTTP / REST → node:http(標準)。Express は使わない
  • SQLite → node:sqlite(Node 22 以降で標準。ネイティブ拡張が不要になったのが効いた)
  • WebSocket → ws のみ(唯一の外部依存。バージョンは厳密固定)
  • CSV パース、multipart、Shift_JIS デコード → すべて標準(TextDecoder 等)で自前実装

結果、外部依存は実質 ws 一つ。package-lock.json を固定し、npm ci で再現できるようにした。

なお、Node のバージョン管理は nvm と n が二重に入っていてハマったので、nvm に一本化し、プロジェクトごとに .nvmrc を置いた。node:sqlite が安定して使えるのは 22.13 以降だったので、そこも .nvmrc で固定。


ハマったところ:ネットワークを2回落とした

一番の落とし穴はここだった。ミラーポート監視用のインターフェースの扱いで、社内ネットワークを2回ダウンさせた。

Radxa Zero 3E + Armbian では、内蔵 Ethernet のインターフェース名が eth0 でも end0 でもなく end1 だった。これをミラーポート(スイッチのミラー出力)に繋いだところ、社内ネット全体がインターネットに繋がらなくなった。

原因は、ミラー側インターフェースが DHCP を投げようとし、受信したフレームに応答パケットを返して、それがミラー元ネットワークに逆流していたこと。旧機(Pi 2)では、ミラー側に別サブネットの固定 IP を最初から振っていたので、この問題が表面化していなかった。設計上の意味があったわけだ。

対策:

  • ミラー側(end1)を DHCP 無効・別サブネットの固定 IP にする
  • 送信を完全に遮断する(nftables で当該インターフェースの OUTPUT を drop)
  • mDNS もミラー側で応答させない(avahi の deny-interfaces)

これで「受信専用」になり、逆流しなくなった。ミラーポート監視をやる人は、監視側インターフェースを送信できない状態に閉じ込めておくのが安全だと思う。

Armbian は Netplan 経由で systemd-networkd を管理する構成で、既定が match: name "e*" で全 Ethernet に DHCP を投げていた。内蔵 Ethernet が enx+MAC の別名も持っていて match の除外に手こずったので、最終的に Netplan を使わず /etc/systemd/network/ に直接 .network を置いた。


Radxa Zero 3E の実機で気づいたこと

  • 物理スイッチが一つもない。 リセットも、電源も、Maskrom(低レベル復旧モード)ボタンすら無い。3W にはある Maskrom が 3E には無い。
  • したがって microSD が起動不能になったら、抜いて別マシンで焼き直すのが唯一の復旧手段。 ゴールデンイメージが文字通り最後の砦になる。
  • 電源 LED はシャットダウン後、消灯までやや時間がかかるが、最終的には消える。停止確認は ping の応答断か LED 消灯で。
  • /boot は ext4(単一パーティション)。Raspberry Pi のように「カードを挿すだけで FAT の /boot が読める」わけではないので、世代管理はイメージのファイル名でやることにした。
  • USB-C が2つ並んでいて、片方が電源(OTG)、片方がホスト。基板のシルクに USB_OTG / 5V IN と書いてあるが、挿し間違えると起動しないので注意。

物理スイッチが無い環境での固定 IP 設定

旧機(Pi 2)では、GPIO に Matrix LED とタクトスイッチを載せた自作の基板を挿していて、これで IP を LED に表示し、タクトスイッチ長押しで DHCP に戻す、という物理的な非常手段があった。

Zero 3E では、この自作基板は使わない方針にした。物理スイッチも無い。そこで、非常手段をソフトウェアで実現した。

固定 IP を適用するとき、一定時間内に「確定」操作がなければ自動的に元に戻すタイムアウトロールバックを入れた。

  • 適用 → 新 IP で管理画面に再アクセスできたら「確定」→ ロールバック解除
  • 確定しなければ、数分後に自動で元(DHCP)に戻る

肝は、ロールバックをアプリのプロセス内の setTimeout でやらないこと。 アプリが落ちたら復旧しない。systemd-run の一時タイマーで、独立したスクリプトとしてスケジュールした。これなら本体プロセスが死んでも、OS のタイマーが元に戻してくれる。物理タクトスイッチの、より確実な代替になった。


mDNS の -N 現象

セットアップ時の接続に mDNS(ホスト名.local)を使えるようにしたが、運用で -2.local -3.local に化ける現象がある。同じ名前を主張する応答者がいたり、再起動で名前を一瞬手放した隙に起きる、mDNS の仕様上の挙動だ。

過去には別プロダクトで -365.local まで積み上がった例もあった(再起動のたびに番号が増え、過去の名前がネットワークのキャッシュに残って成仏しない)。ここまで来ると人間が試せる範囲を超える。

なので、接続の主軸は固定 IP、mDNS は補助という位置づけにした。日常は .local で十分繋がるが、化けたときの保険として固定 IP を必ず設定できるようにしてある。


「絶対に止まらない」ための可用性設計

旧機は「何か問題が起きたら自動で再起動する」設計だった。病院の電話対応を支えるので、止まってはいけない。これを systemd で再現した。

  • クラッシュ → Restart=on-failure で自動再起動
  • ハング(プロセスは生きているが応答しない)→ ハートビートファイルを定期的に更新し、鮮度が切れたら別タイマーが再起動(旧機の cron 監視スクリプト相当)
  • メモリリーク → cgroup のメモリ使用量を監視し、閾値を超えたら予防的にサービス再起動(旧機は「使用率が一定以上でリブート」していた。今回はボードごと再起動でなくサービス再起動にしてダウン時間を最小化)

ポイントは、「事故による停止」と「意図的な停止」を区別すること。 ライセンス無効化のような意図的な停止では、プロセス自体は生かしたまま、監視系のみを内部で止める。プロセスが生きているので systemd から見れば正常=自動再起動の対象にならず、再起動ループに陥らない。


ケースは 3D プリントで

専用ケースの既製品がほぼ無いので、コミュニティ製の STL(Radxa Zero 3E 用、蓋とベースの2部品)を 3D プリントすることにした。

ここで、日本と中国の受託の価格差を実感した。

  • 日本の受託(MJF / PA12 ナイロン) … 2部品で約 6,000 円
  • 中国の受託(同じ MJF / PA12、色は黒) … 2部品で本体 6 ドルほど、送料込みでも 12 ドル(約 1,800 円)

プリント基板を中国に発注したときとまったく同じ構図だった。品質・精度が本当に要る領域は国内の業者に分があるが、こうした「普通に良いもの」を1個〜少量作るなら、中国の受託が圧倒的に安い。量産するならまとめて発注すれば送料も薄まる。

(教訓:慣れない領域では、AI や検索の最初の提案を鵜呑みにせず、一度「他に安い選択肢は?」と調べる余白を作ったほうがいい。急いで日本の受託で1個発注してしまった後で気づいた。)


納品用イメージの作り方

最後に、工場出荷状態のゴールデンイメージを作った。

  • 検証で入れた設定・データ(ライセンス状態、テスト顧客、履歴、シェル履歴、個人情報を含むバックアップ等)をすべてクリアして工場出荷状態に戻す
  • dd で microSD からイメージを採取し、SHA-256 を記録、圧縮して保管
  • リポジトリにバージョンタグを打ち、イメージ・タグ・コードのコミットが一致するようにした(「このイメージはどのソースから作られたか」が後から追える)
  • イメージとチェックサムを、ローカルとクラウドの2箇所に保管

Maskrom が無い=焼き直しが唯一の復旧手段なので、このイメージが失われると製品を再現できない。複数箇所への保管は必須。


まとめ

  • Raspberry Pi 2 → Radxa Zero 3E への移行は、有線 LAN 内蔵・技適不要・供給保証、という点で妥当だった
  • 先に正本化とテストデータ採取をやったことで、新実装の等価性をテストで担保でき、迷わず進められた
  • ハマったのは主にネットワーク(ミラー側インターフェースの逆流)。監視側は送信遮断で受信専用に閉じ込めるのが安全
  • 物理スイッチが無い分は、ソフトウェア(タイムアウトロールバック、systemd の監視)で補える
  • 開封から配れるイメージまで、実質3日ほど。AIのお陰であることは言うまでもない。

同じように「古い SBC で動いている自作機器を、新しいボードに載せ替えたい」人の参考になれば。特にミラーポート監視をやる場合、監視側インターフェースの扱いには気をつけてほしい。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?