2403195_ja-JP

取消
显示结果 
显示  仅  | 搜索替代 
您的意思是: 

2403195_ja-JP

2403195_ja-JP

FRDM-IMX95でWi-Fi接続を自動で行うスクリプトの書き方 (日本語ブログ)

1. はじめに


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依存型のホストドライバモジュールです。

 

<目次>

 

2.  完成版スクリプト (connect_wifi.sh)


まず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

 

3. 各ブロックの解説


3.1 前提チェック — 静かに失敗させない

set -uo pipefail

'-u'オプション は未定義変数の参照をエラーにし、'-o pipefail' はパイプ内のいずれかのコマンドが失敗した場合に終了コードへ反映します。

なお、あえて '-e'(エラーで即終了)は付けていません。ネットワーク系のコマンドは「失敗しても後続の診断を続けたい」ケースが多く、'-e' があると失敗した瞬間に何のメッセージも出さずにスクリプトが終わってしまうためです。代わりに、各コマンドの直後で '$?' を明示的にチェックし、どの段階で失敗したかを '[OK]' / '[ERROR]' のログとして残す方針にしています。

root権限チェックと認証情報ファイルの存在チェックも、後続処理が意味不明なエラーで落ちる前に、原因を明確にして早期終了させるためのものです。


3.2 認証情報の読み込み — クオートという落とし穴

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' から一切進まないという、原因の分かりにくい症状になります。

この防御処理を入れておけば、認証情報ファイルの記法がクオートあり・なしのどちらでも正しく動作します。


3.3 ドライバのロードとインターフェース起動

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しておきます。


3.4 PSKをPBKDF2で計算 — wpa_passphraseを使わない理由

通常、WPA2-PSKの設定生成には 'wpa_passphrase' コマンドを使います。しかし組み込み環境では2つの問題に直面しました。

  1. パスワードの露出です。'wpa_passphrase SSID PASSWORD' のように引数で渡すと、実行中に 'ps' コマンドや '/proc//cmdline' から平文パスワードが見えてしまいます。
  2. 標準入力経由での動作不良です。露出を避けるため 'wpa_passphrase "$SSID" <<< "$PASSWORD"' とヒアストリングで渡すと、環境によっては次のエラーが出て空のファイルが生成されました。
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

計算が終わったら、平文パスワードを保持する変数は速やかに破棄します。


3.5 wpa_supplicant.conf の生成 — ctrl_interfaceを忘れない

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' で他ユーザーから読めないようにします。


3.6 wpa_supplicant の起動 — 既存プロセスの掃除

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 &

 

3.7 接続完了のポーリング — 固定sleepにしない

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)の不一致、といった具合です。

 

3.8 DHCPでIPアドレス取得

udhcpc -i "${IFACE}" -n -t 5 -T 3
BusyBoxの 'udhcpc' (Micro DHCP Client) でIPアドレスを取得します。
オプションは、
  • '-n'(リース取得失敗時に終了)
  • '-t 5'(リクエスト再送を最大5回)
  • '-T 3'(再送間隔3秒)

です。これらを指定しないと、DHCPサーバに到達できない環境でスクリプトが無限に待ち続けてしまうため、必ず入れておきます。


3.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

'/etc/resolv.conf' にフォールバック用のDNSサーバを追記します。既に同じ行があれば重複追記しないよう 'grep' でチェックしています。

なお、DHCP取得したDNS情報をネットワーク管理系(udhcpcのデフォルトスクリプトやsystemd-resolvedなど)が '/etc/resolv.conf' に書き込む構成では、手動追記が上書きされることがあります。固定DNSを確実に効かせたい場合は、udhcpc側のフックスクリプトで制御するのが本来は堅実です。

 

4. スクリプトの実行


作成したスクリプトに'chmod'コマンドで実行権限を付加し、実行します。

root@frdm-imx95:~# chmod +x connect_wifi.sh
root@frdm-imx95:~# ./connect_wifi.sh

5. まとめ


FRDM-IMX95上でのWi-Fi自動接続スクリプトを題材に、組み込みLinux特有の注意点を解説しました。デスクトップLinuxでは意識する必要のない、次のようなポイントがつまずきどころになります。

  • 'wpa_passphrase' はヒアストリング入力でTTYエラーになることがあり、PBKDF2の自前計算が確実
  • 'wpa_supplicant.conf''ctrl_interface' がないと 'wpa_cli' で状態を取得できない
  • SSIDのクオートの二重化は 'SCANNING' から進まない原因になる
  • SSIDはPSK計算の入力の一部であり、SSIDの誤りは鍵の不一致として現れる
  • ビルドによっては '-B''-f' を併用できない

同様の課題に取り組まれている方の参考になれば幸いです。


6. 関連情報



=========================

本投稿の「Comment」欄にコメントをいただいても、現在返信に対応しておりません。
お手数をおかけしますが、お問い合わせの際には「NXPへの技術質問 - 問い合わせ方法 (日本語ブログ)」をご参照ください。
(既に弊社NXP代理店、もしくはNXPとお付き合いのある方は、直接担当者へご質問いただいてもかまいません。)

  • FRDM-IMX95に搭載されているWi-Fiモジュールを、起動後に毎回手作業で接続するは非常に煩わしいです。そこで本記事では、起動時に自動でWi-Fi接続するための方法と、スクリプトの書き方例について紹介します。

同様のWi-Fiモジュールを搭載しているi.MXファミリの評価ボードにも流用できます。

(読了:20分)

(作業時間: 10分) ※i.MX向けYocto Linuxをビルド、動作確認している前提


i.MX Processors日本語ブログ
标记 (1)
无评分
版本历史
最后更新:
星期二
更新人: