はじめに
Nerves では、公式に用意されている Nerves System を使うだけであれば、通常は mix.exs に対象機器用の依存関係を追加するだけで済みます。
しかし、やりたい内容によっては Nerves System を自分でカスタマイズしてビルドしたくなることもあると思います。
この記事では、Nerves Systems Builder を使って、USB カメラ対応のカスタム Nerves System を作ります。
今回の例では Raspberry Pi 5 を対象にします。
- 対象機器: Raspberry Pi 5
- System: nerves_system_rpi5
- 作業場所: ~/nerves_systems
- Nerves アプリ: ~/Projects/my_nerves_app
同じ考え方は Raspberry Pi 4 など、他の対象機器にも応用できます。
この記事で作るもの
今回は、最小限の変更で USB カメラを認識し、v4l2-ctl で静止画相当のフレームを取得できるところまで確認します。
最終的に有効にする主な設定は以下です。
BR2_PACKAGE_LIBV4L=y
BR2_PACKAGE_LIBV4L_UTILS=y
CONFIG_USB_VIDEO_CLASS=m
まずは小さく始めて、ffmpeg や配信機能は、最初から入れない方針にします。
全体像
Nerves System のビルド作業と、通常の Nerves アプリのビルド作業は分けて考えると理解しやすいです。
~/nerves_systems/
カスタム Linux / Nerves System をビルドする場所
~/Projects/my_nerves_app/
そのカスタム System を使って Elixir ファームウェアをビルドする場所
もう少し具体的には、次のような関係です。
+-------------------------+
| ~/nerves_systems |
| |
| src/nerves_system_rpi5 | 保存すべき System 側の変更
| o/rpi5 | Buildroot の生成物
+------------+------------+
|
| source o/rpi5/nerves-env.sh
v
+---------------------------+
| ~/Projects/my_nerves_app |
| |
| mix firmware | カスタム System を使ってビルド
+---------------------------+
重要なのは、o/rpi5 は生成物であり、長期的に残すべき変更は src/nerves_system_rpi5 側に保存するという点です。
主なディレクトリ
この記事では、次の構成を前提にします。
~/nerves_systems/
├── config/
│ └── config.exs
├── src/
│ └── nerves_system_rpi5/
└── o/
└── rpi5/
~/Projects/
└── my_nerves_app/
それぞれの意味は以下です。
config/config.exs
nerves_systems がどの System を clone / build するかを指定する
src/nerves_system_rpi5/
実際の Nerves System のソースコード
長期的に残す変更はここに保存し、コミットする
o/rpi5/
Buildroot の生成物
make menuconfig や make linux-menuconfig はここで実行する
必要に応じて削除して再生成できる
~/Projects/my_nerves_app/
通常の Nerves アプリ
USB カメラ専用アプリである必要はない
Linux マシンを準備する
Nerves System のビルドには、ある程度の容量と時間が必要です。
目安として、次のような環境を用意します。
- CPU:
x86_64またはaarch64 - OS: Linux
- 空き容量: 128 GB 以上
Debian 系の Linux であれば、必要なパッケージはおおむね次のように入れられます。
sudo apt update
sudo apt install \
git \
build-essential \
bc \
cmake \
cvs \
wget \
curl \
mercurial \
python3 \
python3-aiohttp \
python3-flake8 \
python3-ijson \
python3-nose2 \
python3-pexpect \
python3-pip \
python3-requests \
rsync \
subversion \
unzip \
gawk \
jq \
squashfs-tools \
libssl-dev \
automake \
autoconf \
libncurses5-dev
Erlang、Elixir、nerves_bootstrap については、通常の Nerves 開発環境と同じです。
mix local.hex
mix local.rebar
mix archive.install hex nerves_bootstrap
nerves_systems を clone する
作業場所は任意ですが、ここではホームディレクトリ配下に nerves_systems を clone します。
cd ~
git clone https://github.com/nerves-project/nerves_systems.git
cd ~/nerves_systems
Raspberry Pi 5 だけをビルド対象にする
設定ファイルを作ります。
cp config/starter-config.exs config/config.exs
config/config.exs を編集して、Raspberry Pi 5 の System だけを残します。
[
systems: [
{:nerves_system_rpi5, "https://github.com/nerves-project/nerves_system_rpi5.git"}
]
]
Raspberry Pi 4 の場合は、たとえば次のようになります。
[
systems: [
{:nerves_system_rpi4, "https://github.com/nerves-project/nerves_system_rpi4.git"}
]
]
複数の System を同時に扱うこともできますが、最初は対象を一つに絞る方が分かりやすいです。
一度そのままビルドする
まず、何も変更せずに一度ビルドします。
cd ~/nerves_systems
mix deps.get
mix ns.clone
mix ns.build
完了すると、次のディレクトリができているはずです。
~/nerves_systems/src/nerves_system_rpi5/
~/nerves_systems/o/rpi5/
確認します。
ls ~/nerves_systems/src/nerves_system_rpi5
ls ~/nerves_systems/o/rpi5
カスタム用の branch を作る
生成物は o/rpi5 にありますが、保存すべき System のソースコードは src/nerves_system_rpi5 にあります。
まず、System 側のソースコードで branch を作ります。
cd ~/nerves_systems/src/nerves_system_rpi5
git checkout -b custom-usb-camera
最初の実験では fork しなくても大丈夫です。ローカル branch で十分です。
長期的に使う場合や、チームで共有する場合は、fork するか、自分たち用の System repository を用意するとよさそうです。
Buildroot の設定を変更する
Buildroot の設定変更は、生成物側のディレクトリで行います。
cd ~/nerves_systems/o/rpi5
make menuconfig
USB カメラの最初の確認では、v4l2-ctl があると便利です。
今回有効にした設定は以下です。
BR2_PACKAGE_LIBV4L=y
BR2_PACKAGE_LIBV4L_UTILS=y
make menuconfig 上では、次のあたりから辿れました。
Target packages
-> Libraries
-> Hardware handling
-> libv4l
-> v4l-utils tools
Buildroot の版によって表示名が少し違う可能性があります。その場合は / で検索し、LIBV4L または v4l を探します。
make menuconfig の基本操作は以下です。
矢印キー 移動
Enter 下位項目へ移動
Space 有効、無効の切り替え
/ 検索
Esc Esc 戻る
Save 一時保存
Exit 終了
終了後、Buildroot の設定を System 側のソースコードへ保存します。
make savedefconfig
差分を確認します。
cd ~/nerves_systems/src/nerves_system_rpi5
git diff nerves_defconfig
期待する差分は、おおむね次のような内容です。
+BR2_PACKAGE_LIBV4L=y
+BR2_PACKAGE_LIBV4L_UTILS=y
Linux kernel の設定を変更する
USB カメラを使うには、Linux kernel 側でも USB Video Class を有効にする必要があります。
こちらも生成物側のディレクトリで作業します。
cd ~/nerves_systems/o/rpi5
make linux-menuconfig
検索で次の語を探します。
USB Video Class
UVC
CONFIG_USB_VIDEO_CLASS
今回の実験では、MEDIA_SUPPORT は既に module として構成されていたため、最小限の有効な変更は次でした。
CONFIG_USB_VIDEO_CLASS=m
make linux-menuconfig 上では、次のあたりから辿れました。
Device Drivers
-> Multimedia support
-> Media drivers
-> Media USB Adapters
-> USB Video Class (UVC)
ここで次を有効にします。
<M> USB Video Class (UVC)
最初の実験では、古い機種や特定機種向けの USB カメラドライバは有効にしませんでした。
< > GSPCA based webcams
< > USB Philips Cameras
< > USB Sensoray 2255 video capture device
< > USBTV007 video capture support
< > Empia EM28xx USB devices support
まずは UVC 対応の一般的な USB カメラを動かすことに集中します。
終了後、Linux kernel の設定を System 側のソースコードへ保存します。
make linux-update-defconfig
差分を確認します。
cd ~/nerves_systems/src/nerves_system_rpi5
git diff linux-*.defconfig
期待する差分は、おおむね次のような内容です。
+CONFIG_USB_VIDEO_CLASS=m
ファイル名は Linux kernel の版に依存します。
今回の実験では、次のファイルでした。
linux-6.12.defconfig
System 側の変更をコミットする
System 側の repository で差分を確認します。
cd ~/nerves_systems/src/nerves_system_rpi5
git status
git diff
問題なければコミットします。
git add nerves_defconfig linux-*.defconfig
git commit -m "Enable USB camera support"
今回の最小構成では、意味のある変更は次でした。
nerves_defconfig
BR2_PACKAGE_LIBV4L=y
BR2_PACKAGE_LIBV4L_UTILS=y
linux-6.12.defconfig
CONFIG_USB_VIDEO_CLASS=m
rootfs_overlay/ などに追加ファイルを置いた場合は、それも一緒にコミットします。
カスタム System を再ビルドする
保存した設定をもとに、System を再ビルドします。
cd ~/nerves_systems
rm -rf o/rpi5
mix ns.build
学習中は、o/rpi5 を消してから再ビルドする方が分かりやすいです。
Buildroot の生成物が古い状態で残っていると、何が反映されているのか分かりにくくなるためです。
Nerves アプリからカスタム System を使う
ここからは通常の Nerves アプリ側の作業です。
新しい terminal を開いて、Nerves アプリのディレクトリへ移動します。
cd ~/Projects/my_nerves_app
先ほどビルドしたカスタム System の環境変数を読み込みます。
. ~/nerves_systems/o/rpi5/nerves-env.sh
export MIX_TARGET=rpi5
その後、通常通りファームウェアをビルドします。
mix deps.get
mix firmware
カスタム System が使われているか確認します。
mix nerves.info
次のように、system と toolchain が ~/nerves_systems/o/rpi5 を指していれば成功です。
system: /home/mnishiguchi/nerves_systems/o/rpi5
toolchain: /home/mnishiguchi/nerves_systems/o/rpi5/host
ファームウェアを書き込む
通常の Nerves の流れで書き込みます。
mix burn
mix firmware.gen.script
./upload.sh nerves.local
使うコマンドは、対象機器やアプリの構成によって変わります。
USB カメラを実機で確認する
ここからは Nerves IEx 上で確認します。
まず、v4l2-ctl が入っているか確認します。
cmd "type v4l2-ctl"
期待する結果は以下の通りです。
v4l2-ctl is /usr/bin/v4l2-ctl
V4L2 device を一覧する
USB カメラを挿した状態で、device を一覧を出します。
cmd "v4l2-ctl --list-devices"
今回の実験では、ELECOM の USB カメラが次のように見えました。
ELECOM 2MP Webcam: ELECOM 2MP W (usb-xhci-hcd.1-1):
/dev/video0
/dev/video1
/dev/media3
Raspberry Pi 5 では、platform 側の video device も多数見えました。
/dev/video19
/dev/video20
...
/dev/video35
USB カメラとして表示された項目の下にある /dev/video0 や /dev/video1 を確認するのが大事です。
正しい video node を確認する
まず /dev/video0 を確認します。
cmd "v4l2-ctl -d /dev/video0 --info"
今回の実験では、/dev/video0 が実際の映像取得用 node でした。
Driver name : uvcvideo
Card type : ELECOM 2MP Webcam: ELECOM 2MP W
Device Caps : 0x04200001
Video Capture
Streaming
Extended Pix Format
次に /dev/video1 も確認します。
cmd "v4l2-ctl -d /dev/video1 --info"
今回の実験では、/dev/video1 は metadata 用でした。
Device Caps : 0x04a00000
Metadata Capture
Streaming
Extended Pix Format
したがって、画像取得には次を使いました。
/dev/video0
Elixir から device node を見る場合は、次のようにも確認できます。
cmd "ls /dev/video*"
Path.wildcard("/dev/video*")
カメラの対応形式を確認する
対応形式を確認します。
cmd "v4l2-ctl -d /dev/video0 --list-formats-ext"
今回の ELECOM の USB カメラでは、主に次の形式が使えました。
YUYV YUYV 4:2:2
MJPG Motion-JPEG, compressed
解像度は、たとえば次のようなものがありました。
1280x720
800x600
640x480
352x288
1600x1200
最初の確認では、MJPG が便利でした。
USB カメラ側が圧縮済みの JPEG 相当の frame を返してくれるため、最初から ffmpeg を追加しなくても、画像として扱いやすいデータを得られるためです。
raw frame を取得する
まず raw frame を保存してみます。
cmd "v4l2-ctl -d /dev/video0 --stream-mmap --stream-count=1 --stream-to=/data/frame.raw"
保存されたか確認します。
cmd "ls -lh /data/frame.raw"
今回の実験では、次のようなファイルができました。
-rw------- 1 root root 1.8M Jun 1 11:19 /data/frame.raw
これで、USB カメラから frame を取得できていることが分かります。
MJPEG / JPEG frame を取得する
次に、MJPG 形式で frame を保存します。
cmd "v4l2-ctl -d /dev/video0 --set-fmt-video=width=1280,height=720,pixelformat=MJPG --stream-mmap --stream-count=1 --stream-to=/data/frame.jpg"
保存されたか確認します。
cmd "ls -lh /data/frame.jpg"
今回の実験では、次のようなファイルができました。
-rw------- 1 root root 61.6K Jun 1 11:21 /data/frame.jpg
これで、USB カメラから JPEG 相当の frame を取得できました。
od が入っていない場合は、Elixir から先頭 bytes を確認できます。
File.read!("/data/frame.jpg")
|> binary_part(0, 4)
|> Base.encode16(case: :lower)
JPEG であれば、通常は次のような先頭になります。
ffd8ff
今回の実験結果
今回の最小構成では、次の設定で USB カメラを使えるところまで確認できました。
BR2_PACKAGE_LIBV4L=y
BR2_PACKAGE_LIBV4L_UTILS=y
CONFIG_USB_VIDEO_CLASS=m
実機では次を確認しました。
v4l2-ctl is /usr/bin/v4l2-ctl
Driver name: uvcvideo
Camera: ELECOM 2MP Webcam
Capture node: /dev/video0
Metadata node: /dev/video1
Raw frame capture: /data/frame.raw
MJPG frame capture: /data/frame.jpg
USB カメラは機種によって対応形式や node の出方が変わります。
そのため、特定の /dev/video0 を決め打ちするよりも、まず v4l2-ctl --list-devices と v4l2-ctl --info で実機の状態を確認するのが良さそうです。
おわりに
Nerves System のカスタムは、最初は少し大げさに見えます。
しかし、今回のように目的を小さくすると、作業の流れはかなり明確になります。
- Buildroot で userland の道具を入れる
- Linux kernel で driver を有効にする
- System 側に設定を保存する
- Nerves アプリ側で custom System を読み込む
- 実機で小さく確認する
USB カメラ対応であれば、最初の到達点は「動画再生」や「配信」ではなく、v4l2-ctl で 1 frame 取得するところに置くのが良さそうです。
そこまで確認できれば、その先に画像保存、定期撮影、MJPEG 配信、Livebook 連携などを積み上げていけます。
![]()
![]()
![]()

