はじめに
Node-RED / Node-RED MCUを使ってIoTデバイスを作る事が多いのですが、センサーデバイスで取得したデータをデバイスの近くに設置したエッジサーバで処理する構成がほとんどです。エッジサーバにはRaspberry Piを使うことが多いのですが、こういったエッジコンピューティングに便利に使う事ができる設定内容を紹介します。
記事の後半に新しいRaspberry Pi OSをインストールした状態から一連の設定を行うためのシェルスクリプトも用意しています。
Raspberry Pi OSのバージョン
Trixieだけでなく、ひとつ前のBookwormでも一応動作確認しています。
Raspberry Pi のハードウェア
Raspberry Pi 5B と、Raspberry Pi 4B で動作しています。
前提条件
初期化したマイクロSDカードにRaspberry Pi OSをインストールします。
その時に、Raspberry Pi Imagerでカスタマイズ設定を適用しておきます。
・ホスト名:仮のホスト名をセットします。(シェルスクリプトで書き換えます)
・city,timezone,keyboard layoutなどを設定します
・ユーザー名、ログインパスワード:任意のユーザー名とパスワード
・WiFi SSID、パスワード: 利用可能なWiFiの接続情報
・ssh:有効にしておいた方が後で便利です。
・Raspberry Pi Connect: 無効にしておきます。
設定項目
1. ホスト名の更新
ホスト名はmDNSを使ってネットワーク上でサーバにアクセスするためのアドレスとしても使用されるので、特に複数のエッジサーバを使用する場合は分かりやすくて"重複しない"ホスト名を付ける必要があります。
ここではプリフィックス+[MACアドレスの下4桁]のホスト名に変更します。プリフィックスはシェルスクリプト内で設定ができます。
# Hostname prefix (will be followed by MAC address last 4 digits)
HOSTNAME_PREFIX="pi-iot"
初期状態では"pi-iot"としています。
この場合、MACの下4桁が"abcd"なら、"pi-iot-abcd"といったホスト名が設定され、ネットワーク上ではアドレス:"pi-iot-abcd.local"でアクセスすることができるようになります。
また、次の項目で説明するWiFiアクセスポイントのSSIDも重複を避けられるよう、このホスト名をSSIDとしています。
(頻繁には起きないと思いますが、もしもMACアドレスの下4桁が一致するものを同じネットワーク内で使用する場合は別途変更してください。)
2.WiFiアクセスポイントの設定
通常WiFiはクライアントモードのwlan0で職場や家庭内のWiFiアクセスポイントを経由してインターネットに接続していますが、この接続は有効にしたままRaspberry PiをIoTデバイスが接続できるアクセスポイントにします。
方法はwlan0で使っているWiFiにバーチャルインターフェースを設定して、同じWiFiチップでクライアントモードのwlan0と、アクセスポイントモードのap0の2つのインターフェースを設定します。
この方法はバーチャルインターフェースを使用するので、2つのインターフェースが同じ周波数帯になる制約があります。このため、ap0をIoTデバイス向けの2.4GHzで運用したい場合、wlan0も2.4GHzで設定しておく必要があります。
このようにバーチャルインターフェースのap0をアクセスポイントモードで作成して、IoTデバイスが接続するためのパスワードを設定します。
SSIDについては先の項目で述べたように"pi-iot-abcd"といったホスト名が使われます。
CON_NAMEは内部でNetwork Managerが管理する接続名です。
# Access Point settings
APPASSWORD="p@ssw0rd"
CON_NAME="pi_ap"
記事中のパスワードは「仮」ですので、実行前に変更しておいてください。
Raspberry PiでWiFiアクセスポイントを設定してIoTデバイスを使うときの「現時点での」課題と、当面の回避策盛り込みについて
アクセスポイントに接続してくるIoTデバイスの多くはMCUで動作し、所謂「電源ブツ切り」が良く発生しますが、「現時点の」Raspberry PiのWiFiドライバはこの時にクラッシュする場合があるようです。
スマートフォンのように「電源ブツ切り」の発生しないようなデバイスでは接続を解除する際にお行儀よくdeauthフレームを送信して正常な手順を経て接続を解除しますが、このお行儀のよいデバイスの場合はクラッシュの問題はありません。現在の問題は、IoTデバイスのようにdeauthフレームを送らずに電源断などで接続解除したり、その状態からまた再接続する際に発生しているようです。
この問題はgithubのRaspberry Piリポジトリにもissueの報告をしています。同様の問題に直面している方はWatchしていただけると嬉しいです。早く解決すると良いですね。
このクラッシュが発生すると、バーチャルインターフェースのap0が消えてしまいます。その状態から正常な接続に回復するためにはもう一度ap0の起動をすれば良さそうなので、
・一定時間ごとにap0インターフェースが存在するか確認
・ap0が見つからなかったらap0を起動する
という仕掛けを入れています。
今後raspberry pi側のソフトウェアが改修されてクラッシュが起きなくなった場合でもそれほど悪さはしない(はず)と思います。
3.いま見えていない「現場」のWiFiへの接続設定
wlan0でクライアントモードで接続するWiFiアクセスポイントの設定です。
システムを準備している場所で必ずしも現場のWiFiが見えている訳では無いと思います。エッジサーバとして現場やセミナー会場で使用する場合、現場に持ち込んですぐにその場所のWiFiを経由してインターネットに接続するため、あらかじめ接続するWiFiルーターのSSIDとパスワードを設定しておくことができます。
# Venue WiFi settings (leave empty if not needed)
VENUE_WIFI_SSID=""
VENUE_WIFI_PASSWORD=""
VENUE_WIFI_CONNECTION_NAME="venue-wifi"
4.DNS設定の追加
WiFiを経由してインターネットに接続する際のDNSを追加設定します。
これは、Node-RED MCUのセットアップの際にModdable SDKやESP-IDFのインストールの際にgithubの名前解決ができない事があり、追加しました。
1.1.1.1 : Cloudflare
8.8.8.8 : Google Public DNS
の2つを追加しています。必要に応じて編集してください。
# DNS servers
DNS_SERVERS="8.8.8.8 1.1.1.1"
5.そのほか
Raspberry PiをVNCなどのリモートデスクトップ環境で使用するときに便利な、画面サイズをあらかじめ設定する内容も含んでいます。
(boot/config.txtの内容を修正しています。)
Raspberry Piの設定からVNCを有効にすることでリモートデスクトップ環境が利用できます。
設定用シェルスクリプト(全体)
#!/bin/bash -
set -e # STOP when error
################################################################################
# Configuration Section - Modify these values as needed
################################################################################
# Hostname prefix (will be followed by MAC address last 4 digits)
HOSTNAME_PREFIX="pi-iot"
# Access Point settings
APPASSWORD="p@ssw0rd"
CON_NAME="pi_ap"
# Venue WiFi settings (leave empty if not needed)
VENUE_WIFI_SSID=""
VENUE_WIFI_PASSWORD=""
VENUE_WIFI_CONNECTION_NAME="venue-wifi"
# DNS servers
DNS_SERVERS="8.8.8.8 1.1.1.1"
################################################################################
# Script Start - Do not modify below unless you know what you're doing
################################################################################
###### Get MAC address and create a new host name
echo ">>> Setting New host name"
MAC4=$(ip link show wlan0 2>/dev/null | grep "link/ether" | awk '{print $2}' | sed 's/://g' | tail -c 5)
# fallback to ifconfig
if [ -z "$MAC4" ]; then
MAC4=$(ifconfig 2>/dev/null | grep -A 6 "wlan0" | grep "ether" | head -n 1 | sed "s/:/ /g" | awk '{print $6$7}')
fi
HOST_NAME="${HOSTNAME_PREFIX}-${MAC4}"
echo "host name will be changed to: $HOST_NAME"
###### Create virtual AP interface
# confirm current phy name and wlan0 channel
PHY_NAME=$(iw dev | awk '/phy#/ {print $1}')
MAC_ADDR=$(iw dev wlan0 info | awk '/addr/ {print $2}')
if [ -z "$PHY_NAME" ] || [ -z "$MAC_ADDR" ]; then
echo ">>> can't get info wlan0"
exit 1
fi
echo ">>> PHY_NAME: $PHY_NAME"
echo ">>> MAC_ADDR: $MAC_ADDR"
# confirm if the virtual interface already exists
if iw dev | grep -q "ap0"; then
echo ">>> ap0 already exists."
else
sudo iw dev wlan0 interface add ap0 type __ap
echo ">>> ap0 added."
fi
# create systemd service file for ap0 creation
AP0_SERVICE="/etc/systemd/system/ap0.service"
cat <<EOF | sudo tee $AP0_SERVICE > /dev/null
[Unit]
Description=Create ap0 interface
After=network.target
[Service]
Type=oneshot
ExecStart=/sbin/iw dev wlan0 interface add ap0 type __ap
ExecStart=/sbin/ip link set ap0 address $MAC_ADDR
ExecStart=/bin/ip link set ap0 up
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
echo ">>> Created ap0 Service File"
# create ap0 monitor script
MONITOR_SCRIPT="/usr/local/bin/ap0-monitor.sh"
cat <<'EOF' | sudo tee $MONITOR_SCRIPT > /dev/null
#!/bin/bash
# ap0 interface monitor and recovery script
LOG_FILE="/var/log/ap0-monitor.log"
MAX_LOG_SIZE=1048576 # 1MB
# rotate log if too large
if [ -f "$LOG_FILE" ] && [ $(stat -f%z "$LOG_FILE" 2>/dev/null || stat -c%s "$LOG_FILE") -gt $MAX_LOG_SIZE ]; then
mv "$LOG_FILE" "$LOG_FILE.old"
fi
log_message() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# check if ap0 exists
if ! iw dev | grep -q "ap0"; then
log_message "WARNING: ap0 interface disappeared, attempting recovery..."
# restart ap0 service
systemctl restart ap0.service
sleep 2
# verify ap0 was recreated
if iw dev | grep -q "ap0"; then
log_message "SUCCESS: ap0 interface recovered"
# restart network manager connection
nmcli connection up "pi_ap" 2>&1 | tee -a "$LOG_FILE"
log_message "INFO: pi_ap connection restarted"
else
log_message "ERROR: Failed to recover ap0 interface"
exit 1
fi
else
# ap0 exists, check if it's up
if ! ip link show ap0 | grep -q "UP"; then
log_message "WARNING: ap0 is down, bringing it up..."
/sbin/ip link set ap0 up
sleep 1
nmcli connection up "pi_ap" 2>&1 | tee -a "$LOG_FILE"
fi
fi
EOF
sudo chmod +x $MONITOR_SCRIPT
echo ">>> Created ap0 Monitor Script"
# create systemd service for ap0 monitoring
MONITOR_SERVICE="/etc/systemd/system/ap0-monitor.service"
cat <<EOF | sudo tee $MONITOR_SERVICE > /dev/null
[Unit]
Description=Monitor and recover ap0 interface
After=ap0.service network.target
[Service]
Type=oneshot
ExecStart=$MONITOR_SCRIPT
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
echo ">>> Created ap0 Monitor Service File"
# create systemd timer for periodic monitoring
MONITOR_TIMER="/etc/systemd/system/ap0-monitor.timer"
cat <<EOF | sudo tee $MONITOR_TIMER > /dev/null
[Unit]
Description=Timer for ap0 interface monitoring
Requires=ap0-monitor.service
[Timer]
OnBootSec=2min
OnUnitActiveSec=30s
AccuracySec=10s
[Install]
WantedBy=timers.target
EOF
echo ">>> Created ap0 Monitor Timer File"
###### setting network manager
echo ">>> setting network manager"
SSID=$HOST_NAME
if nmcli connection show "$CON_NAME" > /dev/null 2>&1; then
echo ">>> '$CON_NAME' already exists, deleting..."
sudo nmcli connection delete "$CON_NAME"
fi
sudo nmcli connection add \
type wifi \
ifname ap0 \
con-name "$CON_NAME" \
autoconnect yes \
ssid "$SSID" \
802-11-wireless.mode ap \
802-11-wireless.band bg \
802-11-wireless.channel 1 \
ipv4.method shared \
ipv4.never-default yes \
wifi-sec.key-mgmt wpa-psk \
wifi-sec.pairwise ccmp \
wifi-sec.group ccmp \
wifi-sec.proto rsn \
wifi-sec.psk "$APPASSWORD"
###### Create venue WiFi connection (if configured)
if [ -n "$VENUE_WIFI_SSID" ] && [ -n "$VENUE_WIFI_PASSWORD" ]; then
echo ">>> Setting up venue WiFi connection"
if nmcli connection show "$VENUE_WIFI_CONNECTION_NAME" > /dev/null 2>&1; then
echo ">>> '$VENUE_WIFI_CONNECTION_NAME' already exists, deleting..."
sudo nmcli connection delete "$VENUE_WIFI_CONNECTION_NAME"
fi
sudo nmcli connection add \
type wifi \
ifname wlan0 \
con-name "$VENUE_WIFI_CONNECTION_NAME" \
ssid "$VENUE_WIFI_SSID" \
wifi-sec.key-mgmt wpa-psk \
wifi-sec.psk "$VENUE_WIFI_PASSWORD" \
802-11-wireless.band bg \
autoconnect yes
echo ">>> Venue WiFi connection created (will auto-connect when in range)"
fi
echo ">>> setting DNS for existing connections"
# wlan0 DNS setting (search by device name for compatibility)
WLAN0_CONNECTION=$(nmcli -t -f NAME,DEVICE connection show --active | grep ':wlan0$' | cut -d: -f1)
if [ -n "$WLAN0_CONNECTION" ]; then
echo ">>> setting DNS for '$WLAN0_CONNECTION' (wlan0)"
sudo nmcli connection modify "$WLAN0_CONNECTION" ipv4.dns "$DNS_SERVERS"
sudo nmcli connection modify "$WLAN0_CONNECTION" ipv4.ignore-auto-dns yes
else
# If not active, search all connections for wlan0
WLAN0_CONNECTION=$(nmcli -t -f NAME,DEVICE connection show | grep ':wlan0$' | cut -d: -f1 | head -n 1)
if [ -n "$WLAN0_CONNECTION" ]; then
echo ">>> setting DNS for '$WLAN0_CONNECTION' (wlan0)"
sudo nmcli connection modify "$WLAN0_CONNECTION" ipv4.dns "$DNS_SERVERS"
sudo nmcli connection modify "$WLAN0_CONNECTION" ipv4.ignore-auto-dns yes
else
echo ">>> wlan0 connection not found, skipping"
fi
fi
# eth0 (Wired connection 1) DNS setting
if nmcli connection show "Wired connection 1" > /dev/null 2>&1; then
echo ">>> setting DNS for 'Wired connection 1' (eth0)"
sudo nmcli connection modify "Wired connection 1" ipv4.dns "$DNS_SERVERS"
sudo nmcli connection modify "Wired connection 1" ipv4.ignore-auto-dns yes
else
echo ">>> 'Wired connection 1' not found, skipping"
fi
# Venue WiFi DNS setting (if configured)
if [ -n "$VENUE_WIFI_CONNECTION_NAME" ] && nmcli connection show "$VENUE_WIFI_CONNECTION_NAME" > /dev/null 2>&1; then
echo ">>> setting DNS for '$VENUE_WIFI_CONNECTION_NAME'"
sudo nmcli connection modify "$VENUE_WIFI_CONNECTION_NAME" ipv4.dns "$DNS_SERVERS"
sudo nmcli connection modify "$VENUE_WIFI_CONNECTION_NAME" ipv4.ignore-auto-dns yes
fi
echo ">>> network manager setting finished."
###### HDMI headless mode
# setting boot/config.txt for HDMI headless mode
if ! ls /boot/config.txt.bak > /dev/null 2>&1; then
sudo cp /boot/config.txt /boot/config.txt.bak
fi
sudo sed -i 's/^#hdmi_force_hotplug=1/hdmi_force_hotplug=1/' /boot/config.txt
sudo sed -i 's/^#hdmi_group=1/hdmi_group=2/' /boot/config.txt
sudo sed -i 's/^#hdmi_mode=1/hdmi_mode=82/' /boot/config.txt
sudo sed -i 's/^#hdmi_drive=2/hdmi_drive=2/' /boot/config.txt
###### enable rules and services
sudo rfkill unblock wifi
sudo systemctl daemon-reload
sudo systemctl enable ap0.service
sudo systemctl enable ap0-monitor.service
sudo systemctl enable ap0-monitor.timer
###### Detect OS version
if [ -f /etc/os-release ]; then
. /etc/os-release
VERSION_CODENAME=${VERSION_CODENAME:-unknown}
echo "Detected OS: $PRETTY_NAME (codename: $VERSION_CODENAME)"
else
VERSION_CODENAME="unknown"
fi
###### Apply host settings based on OS version
if [ "$VERSION_CODENAME" = "trixie" ]; then
echo "Using Trixie (Debian 13) method..."
# 1. Update cloud-init user-data
if [ -f /boot/firmware/user-data ]; then
echo " - Updating cloud-init hostname..."
sudo sed -i "s/^hostname:.*/hostname: $HOST_NAME/" /boot/firmware/user-data
else
echo " - Warning: /boot/firmware/user-data not found"
fi
# 2. Update systemd hostname
echo " - Setting hostname via hostnamectl..."
sudo hostnamectl set-hostname "$HOST_NAME"
# 3. Update /etc/hostname (for consistency)
echo " - Updating /etc/hostname..."
echo "$HOST_NAME" | sudo tee /etc/hostname > /dev/null
# 4. Update /etc/hosts
echo " - Updating /etc/hosts..."
sudo sed -i "s/^127\.0\.1\.1[[:space:]]\+.*/127.0.1.1 $HOST_NAME/" /etc/hosts
else
echo "Using traditional method (Bookworm and earlier)..."
# Traditional method
echo " - Updating /etc/hostname..."
echo "$HOST_NAME" | sudo tee /etc/hostname > /dev/null
echo " - Updating /etc/hosts..."
sudo sed -i "s/^127\.0\.1\.1[[:space:]]\+.*/127.0.1.1 $HOST_NAME/" /etc/hosts
fi
echo ""
echo "Hostname changed successfully to: $HOST_NAME"
echo ""
echo "=== Setup Summary ==="
echo "- Hostname: $HOST_NAME"
echo "- AP SSID: $SSID"
echo "- AP Password: $APPASSWORD"
echo "- ap0 interface service: enabled"
echo "- ap0 monitoring service: enabled (checks every 30 seconds)"
echo "- Monitor log: /var/log/ap0-monitor.log"
if [ -n "$VENUE_WIFI_SSID" ]; then
echo "- Venue WiFi: $VENUE_WIFI_SSID (pre-configured)"
fi
echo ""
echo -n "Process finished. You should reboot now. [Y/n]: "
read ANS
case $ANS in
"" | [Yy]* )
sudo reboot
;;
* )
echo "Please reboot manually to apply changes."
;;
esac
上記の内容を、例えばpi_setup_IoT.shのような.shファイルに保存して、実行権限を与えてから実行してください。
最後に再起動すると、ホスト名が変更されてWiFiアクセスポイントが稼働しています。
Raspberry Pi 側で、例えばMQTT Brokerノードを設置して運用すると、Node-RED MCUを使ってプログラムするセンサ等のデバイスからはpi-iot-abcd.localといったサーバアドレスを指定して"MQTT in" や"MQTT out"ノードからブローカーにアクセスすることができます。
さいごに
私の所では、新しいRaspberry Pi用のSDカードをセットアップする際はまずこのシェルスクリプトを走らせて、それに続いてModdable SDKとESP-IDFのインストールを行い、Node-RED, Node-RED MCU, MCUノードなどのインストールを行って環境を構築しています。
シェルスクリプトの改良点などありましたらコメント頂けると嬉しいです。

