FRDM-IMX95のようなボードで開発を進めていると、起動後に毎回手作業でWi-Fiに接続するのが煩わしくなってきます。そこで、モジュールのロードからDHCPによるIPアドレス取得までを一気に行うシェルスクリプト connect_wifi.sh を用意すると便利です。
一見単純なタスクですが、組み込みLinux環境(特にNXPの *moal ドライバや、機能を絞った wpa_supplicant ビルド)では、デスクトップLinuxでは遭遇しないいくつかの落とし穴があります。ここでは、実際に動作するスクリプト(connect_wifi.sh)を1行ずつ解説しながら、それぞれの処理がなぜ必要なのかを説明します。
対象は、i.MX 8M / i.MX 93 / i.MX 95 などNXP SoC上でYoctoベースのLinuxを扱う開発者を想定しています。
*MOAL(MAC OS/A-kernel/OS-adaptor Layer)ドライバとは?
主にNXP Semiconductors(旧Marvell)製の無線LAN(Wi-Fi)チップセットにおいて、LinuxやAndroidなどのOS上で動作するOS依存型のホストドライバモジュールです。
まずconnect_wifi.shの全体像を示します。以降のセクションで各ブロックを順に解説します。
#!/bin/bash
set -uo pipefail
readonly CRED_FILE="/etc/wifi/wifi.conf"
readonly WPA_CONF="/etc/wpa_supplicant.conf"
readonly IFACE="mlan0"
# --- 1. 前提チェック ---
if [[ ${EUID} -ne 0 ]]; then
echo "[ERROR] root権限で実行してください" >&2
exit 1
fi
if [[ ! -f "${CRED_FILE}" ]]; then
echo "[ERROR] 認証情報ファイルが見つかりません: ${CRED_FILE}" >&2
exit 1
fi
# --- 2. 認証情報の読み込み ---
SSID=$(grep -E '^SSID=' "${CRED_FILE}" | cut -d= -f2-)
PASSWORD=$(grep -E '^PASSWORD=' "${CRED_FILE}" | cut -d= -f2-)
# 前後のクオートを防御的に除去
SSID="${SSID%\"}"; SSID="${SSID#\"}"
SSID="${SSID%\'}"; SSID="${SSID#\'}"
PASSWORD="${PASSWORD%\"}"; PASSWORD="${PASSWORD#\"}"
PASSWORD="${PASSWORD%\'}"; PASSWORD="${PASSWORD#\'}"
if [[ -z "${SSID}" || -z "${PASSWORD}" ]]; then
echo "[ERROR] SSID または PASSWORD が読み取れません" >&2
exit 1
fi
echo "[OK] 認証情報を読み込みました (SSID=${SSID})"
# --- 3. ドライバのロードとインターフェース起動 ---
modprobe moal mod_para=nxp/wifi_mod_para.conf
echo "[OK] モジュールをロードしました"
ip link set "${IFACE}" up 2>/dev/null
echo "[OK] ${IFACE} をupにしました"
# --- 4. PSKをPBKDF2で計算 ---
PSK_HEX=$(python3 -c "
import sys, hashlib, binascii
ssid = sys.argv[1]
passphrase = sys.stdin.readline().rstrip('\n')
psk = hashlib.pbkdf2_hmac('sha1', passphrase.encode(), ssid.encode(), 4096, 32)
print(binascii.hexlify(psk).decode())
" "${SSID}" <<< "${PASSWORD}")
unset PASSWORD
if [[ -z "${PSK_HEX}" ]]; then
echo "[ERROR] PSKの計算に失敗しました" >&2
exit 1
fi
# --- 5. wpa_supplicant.conf の生成 ---
cat > "${WPA_CONF}" <<EOF
ctrl_interface=/var/run/wpa_supplicant
ctrl_interface_group=0
network={
ssid="${SSID}"
psk=${PSK_HEX}
}
EOF
chmod 600 "${WPA_CONF}"
# --- 6. wpa_supplicant の起動 ---
pkill -f "wpa_supplicant.*${IFACE}" 2>/dev/null
sleep 1
wpa_supplicant -B -i "${IFACE}" -c "${WPA_CONF}"
if [[ $? -ne 0 ]]; then
echo "[ERROR] wpa_supplicantの起動に失敗しました" >&2
exit 1
fi
echo "[OK] wpa_supplicantを起動しました"
# --- 7. 接続完了のポーリング ---
connected=0
for i in $(seq 1 20); do
state=$(wpa_cli -i "${IFACE}" status 2>/dev/null | grep ^wpa_state | cut -d= -f2)
echo " [${i}/20] wpa_state=${state:-unknown}"
if [[ "${state}" == "COMPLETED" ]]; then
connected=1
break
fi
sleep 1
done
if [[ ${connected} -eq 0 ]]; then
echo "[ERROR] Wi-Fi認証に失敗しました(タイムアウト)" >&2
exit 1
fi
echo "[OK] Wi-Fi認証に成功しました"
# --- 8. DHCPでIPアドレス取得 ---
udhcpc -i "${IFACE}" -n -t 5 -T 3
if [[ $? -ne 0 ]]; then
echo "[ERROR] DHCPによるIPアドレス取得に失敗しました" >&2
exit 1
fi
echo "[OK] DHCPでIPアドレスを取得しました"
# --- 9. DNS設定 ---
if ! grep -q "nameserver 8.8.8.8" /etc/resolv.conf 2>/dev/null; then
echo "nameserver 8.8.8.8" >> /etc/resolv.conf
fi
echo "=== 接続完了 ==="
ip addr show "${IFACE}"
認証情報は、スクリプト本体とは分離した /etc/wifi/wifi.conf に置きます。
root@frdm-imx95:~# mkdir -p /etc/wifi
root@frdm-imx95:~# cat > /etc/wifi/wifi.conf <<'EOF'
SSID=exampleSSID
PASSWORD=examplePassword
EOF
認証情報は他のユーザーからアクセスできないよう、権限を変更しておきます。
root@frdm-imx95:~# chmod 600 /etc/wifi/wifi.conf
set -uo pipefail
'-u'オプション は未定義変数の参照をエラーにし、'-o pipefail' はパイプ内のいずれかのコマンドが失敗した場合に終了コードへ反映します。
なお、あえて '-e'(エラーで即終了)は付けていません。ネットワーク系のコマンドは「失敗しても後続の診断を続けたい」ケースが多く、'-e' があると失敗した瞬間に何のメッセージも出さずにスクリプトが終わってしまうためです。代わりに、各コマンドの直後で '$?' を明示的にチェックし、どの段階で失敗したかを '[OK]' / '[ERROR]' のログとして残す方針にしています。
root権限チェックと認証情報ファイルの存在チェックも、後続処理が意味不明なエラーで落ちる前に、原因を明確にして早期終了させるためのものです。
SSID=$(grep -E '^SSID=' "${CRED_FILE}" | cut -d= -f2-)
認証情報ファイルから 'SSID=' で始まる行を取り出し、'=' 以降を値として抽出します。'cut -d= -f2-' の '-f2-'(2フィールド目以降すべて)がポイントで、これによりパスワードに '=' が含まれていても正しく取り出せます。
続く4行のクオート除去が、実は本スクリプトで最も重要な防御処理です。
SSID="${SSID%\"}"; SSID="${SSID#\"}"
これはbashのパラメータ展開で、'${var%\"}' が末尾のダブルクオート、'${var#\"}' が先頭のダブルクオートを除去します。
なぜ必要かというと、認証情報ファイルにうっかり 'SSID="exampleSSID"' とクオート付きで書いてしまった場合、'cut' はクオートも含めて値として取り込みます。その状態で後段の 'wpa_supplicant.conf' 生成時に 'ssid="${SSID}"' とさらにクオートを付けると、
ssid=""exampleSSID""
という二重クオートになります。'wpa_supplicant' のパーサーは外側の1組しか想定していないため、内側のクオートまでSSIDの一部として解釈してしまい、スキャン結果に存在するはずのAPとマッチしません。結果として 'wpa_state' が 'SCANNING' から一切進まないという、原因の分かりにくい症状になります。
この防御処理を入れておけば、認証情報ファイルの記法がクオートあり・なしのどちらでも正しく動作します。
modprobe moal mod_para=nxp/wifi_mod_para.conf
ip link set "${IFACE}" up 2>/dev/null
NXPのWi-Fiは 'moal' カーネルモジュールで提供され、'mod_para' でファームウェアの動作パラメータファイルを指定します。ロード後にインターフェース(ここでは 'mlan0')を明示的にupしておきます。
通常、WPA2-PSKの設定生成には 'wpa_passphrase' コマンドを使います。しかし組み込み環境では2つの問題に直面しました。
reading passphrase from stdin
tcgetattr: Inappropriate ioctl for device
これは 'wpa_passphrase' が端末のエコーを制御しようと 'tcgetattr()' を呼ぶものの、標準入力が実端末(TTY)ではないために失敗し、その後のパスフレーズ読み込みも中断されるためです。
そこで、WPA2-PSKの鍵導出仕様をそのままpython3で実装しました。
psk = hashlib.pbkdf2_hmac('sha1', passphrase.encode(), ssid.encode(), 4096, 32)
WPA2-PSKのPSKは、仕様上 PBKDF2-HMAC-SHA1(パスフレーズ, SSID, 4096回, 256bit) という決まった計算で導出されます。これを直接計算することで、'wpa_passphrase' のTTY依存を完全に回避できます。パスワードは引数ではなく 'sys.stdin' から受け取るため 'ps' にも露出しません。
unset PASSWORD
計算が終わったら、平文パスワードを保持する変数は速やかに破棄します。
cat > "${WPA_CONF}" <<EOF
ctrl_interface=/var/run/wpa_supplicant
ctrl_interface_group=0
network={
ssid="${SSID}"
psk=${PSK_HEX}
}
EOF
このブロックで見落としやすいのが冒頭の `ctrl_interface` の指定です。
'wpa_cli' は '/var/run/wpa_supplicant/<インターフェース名>' というUNIXドメインソケット経由で 'wpa_supplicant' と通信します。このソケットは 'ctrl_interface' を設定ファイルに書かないと生成されません。これを忘れると、後段のポーリング('wpa_cli status')が次のエラーで動かず、実際には接続に成功していても状態を取得できないため「失敗」と誤判定してしまいます。
Failed to connect to non-global ctrl_ifname: mlan0 error: No such file or directory
また、'psk=' 行の値('PSK_HEX')は16進のハッシュ値なのでクオートを付けません。クオートを付けると平文パスフレーズとして再解釈されてしまうため注意が必要です。一方 'ssid=' は文字列なのでクオートで囲みます。
生成後は 'chmod 600' で他ユーザーから読めないようにします。
pkill -f "wpa_supplicant.*${IFACE}" 2>/dev/null
sleep 1
wpa_supplicant -B -i "${IFACE}" -c "${WPA_CONF}"
再実行時に古い 'wpa_supplicant' プロセスが残っていると、新しいプロセスとソケットが競合したり、古い設定のまま動き続けたりします。起動前に 'pkill' で確実に掃除しておきます。'-B' はバックグラウンド実行を指定するオプションです。
デバッグ時にログをファイルへ出したくなりますが、ビルドによっては '-B'(バックグラウンド)と '-f'(ログファイル)を併用できないことがあります('-f' 未サポートのビルドでは引数解析に失敗し、ヘルプが表示されて起動しません)。その場合は次のようにシェルのリダイレクトを使います。
wpa_supplicant -dd -i "${IFACE}" -c "${WPA_CONF}" > /var/log/wpa_supplicant.log 2>&1 &
for i in $(seq 1 20); do
state=$(wpa_cli -i "${IFACE}" status 2>/dev/null | grep ^wpa_state | cut -d= -f2)
echo " [${i}/20] wpa_state=${state:-unknown}"
if [[ "${state}" == "COMPLETED" ]]; then
connected=1
break
fi
sleep 1
done
電波状況によって認証完了までの時間は変動するため、固定待機は「まだ繋がっていないのに次へ進む」「無駄に待ちすぎる」のどちらかになりがちです。
代わりに 'wpa_cli status' の 'wpa_state' を1秒間隔でポーリングし、'COMPLETED' になった時点で先へ進みます。各ステップで状態を出力しているので、認証がどのフェーズで止まっているか('SCANNING' / 'ASSOCIATING' / '4WAY_HANDSHAKE' など)がリアルタイムに見えるのも利点です。
'wpa_state' の正常な遷移は次のとおりです。
DISCONNECTED → SCANNING → AUTHENTICATING → ASSOCIATING
→ ASSOCIATED → 4WAY_HANDSHAKE → GROUP_HANDSHAKE → COMPLETED
どこで止まるかによって原因の切り分けができます。'SCANNING' から進まなければAPが見つかっていない(SSID誤り、電波、バンド設定など)、'4WAY_HANDSHAKE' で 'WRONG_KEY' が出れば鍵(パスワードまたはSSID)の不一致、といった具合です。
udhcpc -i "${IFACE}" -n -t 5 -T 3
です。これらを指定しないと、DHCPサーバに到達できない環境でスクリプトが無限に待ち続けてしまうため、必ず入れておきます。
if ! grep -q "nameserver 8.8.8.8" /etc/resolv.conf 2>/dev/null; then
echo "nameserver 8.8.8.8" >> /etc/resolv.conf
fi
'/etc/resolv.conf' にフォールバック用のDNSサーバを追記します。既に同じ行があれば重複追記しないよう 'grep' でチェックしています。
なお、DHCP取得したDNS情報をネットワーク管理系(udhcpcのデフォルトスクリプトやsystemd-resolvedなど)が '/etc/resolv.conf' に書き込む構成では、手動追記が上書きされることがあります。固定DNSを確実に効かせたい場合は、udhcpc側のフックスクリプトで制御するのが本来は堅実です。
作成したスクリプトに'chmod'コマンドで実行権限を付加し、実行します。
root@frdm-imx95:~# chmod +x connect_wifi.sh
root@frdm-imx95:~# ./connect_wifi.sh
FRDM-IMX95上でのWi-Fi自動接続スクリプトを題材に、組み込みLinux特有の注意点を解説しました。デスクトップLinuxでは意識する必要のない、次のようなポイントがつまずきどころになります。
同様の課題に取り組まれている方の参考になれば幸いです。
=========================
本投稿の「Comment」欄にコメントをいただいても、現在返信に対応しておりません。
お手数をおかけしますが、お問い合わせの際には「NXPへの技術質問 - 問い合わせ方法 (日本語ブログ)」をご参照ください。
(既に弊社NXP代理店、もしくはNXPとお付き合いのある方は、直接担当者へご質問いただいてもかまいません。)
同様のWi-Fiモジュールを搭載しているi.MXファミリの評価ボードにも流用できます。
(読了:20分)
(作業時間: 10分) ※i.MX向けYocto Linuxをビルド、動作確認している前提