라즈베리파이 WiFi 복원력 + 부팅 1분 31초 → 33초

라즈베리파이로 산업용 디바이스 만들 때 WiFi + 부팅 시간이 의외로 큰 이슈다. 둘 다 사용자가 직접 보고 느끼는 부분이라 첫인상을 좌우한다.

문제 1 — WiFi 자격증명이 사라진다

증상: 처음 WiFi 비번 입력하면 잘 접속됨. 재부팅 후 → 연결 안 됨. NetworkManager 프로필이 사라졌거나 default 로 돌아가 있음.

WiFi 관리 페이지 — 스캔 / 접속 / 저장

원인 다양함:

NM 의 disk persistence 만 믿을 수 없다. 우리 쪽 백업이 필요하다.

해결: wifi-saved.json 자체 저장소

// Software/System/WifiCredentialStore.cs
public sealed class WifiCredentialStore
{
    private readonly string _path;  // {ContentRoot}/wifi-saved.json

    public sealed record SavedNetwork(string Ssid, string? Password, DateTime SavedAt);

    public Store Save(string ssid, string? password)
    {
        var store = Load();
        store.Networks.RemoveAll(n => n.Ssid == ssid);
        store.Networks.Add(new SavedNetwork(ssid, password, DateTime.UtcNow));
        File.WriteAllText(_path, JsonSerializer.Serialize(store, JsonOpts));
        File.SetUnixFileMode(_path, UnixFileMode.UserRead | UnixFileMode.UserWrite);
        return store;
    }
}

WifiService 가 nmcli connect 성공 시 이 store 에도 자동 저장. 0600 권한이라 평문이지만 owner 만 읽음 (이미 root 접근하면 어차피 끝).

부팅 시 복원:

// On service startup (Program.cs)
_ = Task.Run(async () =>
{
    await Task.Delay(3000);  // let systemd + NM settle
    var wifi = app.Services.GetRequiredService<WifiService>();
    await wifi.RestoreSavedAsync();
});

// WifiService.RestoreSavedAsync
public async Task RestoreSavedAsync(CancellationToken ct = default)
{
    var saved = _store.Load();
    var (ok, list) = await RunAsync("nmcli", "-t -f NAME connection show");
    var existing = new HashSet<string>(/* parse list */);

    foreach (var n in saved.Networks)
    {
        if (existing.Contains(n.Ssid)) continue;  // NM already has it
        _log.LogInformation("WiFi restore: '{Ssid}' missing — re-registering", n.Ssid);
        await RunAsync("nmcli", $"device wifi connect \"{n.Ssid}\" password \"{n.Password}\"");
    }
}

부팅 3초 후 백그라운드로 실행 → HTTP 서버 안 막음. NM 에 프로필 이미 있으면 skip (오버헤드 0). 사라졌으면 우리 백업으로 자동 재등록.

이미지에 깔린 후 처음 부팅하든, SD 카드 복제 후든, NM 캐시 깨졌든 — 사용자는 그냥 평소처럼 동작하는 것처럼 보임.

문제 2 — 부팅이 1분 31초

systemd-analyze blame 으로 본 범인:

1min 190ms NetworkManager-wait-online.service       ← 1분
    8.735s zpi-controller.service
    6.347s fstrim.service
    4.912s NetworkManager.service
    ...

NetworkManager-wait-online.service 가 60초 timeout 까지 기다리다 실패하고 다음 부팅 절차로 진행.

왜? WiFi 가 끊긴 상태로 부팅 → NM 이 autoconnect 시도 → 다 실패 → “online” 도달 못 함 → wait-online 이 60초 max 기다림 → 결국 포기.

network-online.target 을 기다리는 서비스가 있으면 다 같이 60초 멈춤. zpi-controller.serviceAfter=network-online.target 이었으니 적어도 60초 후에야 시작됨.

해결 — 두 갈래

1. 우리 서비스가 wait-online 안 기다리게

# /etc/systemd/system/zpi-controller.service
[Unit]
Description=ZPi Controller Service
# 변경: network-online.target → network.target
After=network.target NetworkManager.service
Wants=network.target NetworkManager.service

기본 network 만 기다림 (네트워크 초기화 완료). 실제 인터넷 도달까지는 안 기다림. 우리 서비스는 백그라운드로 WifiService.RestoreSavedAsync() 실행해서 알아서 살아남음.

2. wait-online 서비스 자체 mask

sudo systemctl disable NetworkManager-wait-online.service
sudo systemctl mask NetworkManager-wait-online.service

/dev/null 심볼릭링크로 만들어서 systemd 가 영영 시작 못 함. 다른 서비스가 Wants=network-online.target 으로 우회 의존할 수 있는데, 그 경우에도 같은 효과 (그냥 즉시 도달).

결과

Before: 1min 31.88s (kernel 5.7s + userspace 1min 26s)
After : 33.39s     (kernel 5.5s + userspace 27.9s)

60초 단축. graphical.target 도달 27초로 정상화.

systemd-analyze blame 도 정상:

8.293s zpi-controller.service       ← 이제 가장 느린 것
4.938s NetworkManager.service
4.068s cloud-init-main.service
2.095s dev-mmcblk0p2.device
1.757s accounts-daemon.service

문제 3 — 호스트명 변경 시 mDNS 가 새 이름 광고 안 함

4편 (세 가지 함정) 에서 다룬 내용. avahi-daemon 의 reload 가 호스트명 변경을 picked up 안 함. restart 로 변경 후 자체검증 추가.

합쳐서 — “운영자가 보는 첫인상”

이 세 가지 정리하면:

체감 차이가 크다. 재부팅이 빠르고 / 네트워크가 항상 살아있고 / 이름 바꾸는 게 즉시 반영 되는 디바이스는 신뢰감이 다르다.

양산 자동 명명 보너스

같은 SD 이미지로 보드 여러 대 만들 때 호스트명 충돌 방지를 위해 첫 부팅 시 자동 명명:

# scripts/zpi-uniquify.sh - 양산 첫 부팅 트리거
IP=$(hostname -I | awk '{print $1}')
LAST_OCTET=$(echo "${IP:-0}" | awk -F'.' '{print $NF}')
NEW_HOST="going-zpi-${LAST_OCTET}"
hostnamectl set-hostname "$NEW_HOST"

IP 끝자리(LAST_OCTET)가 그대로 호스트명 꼬리가 된다. 예를 들어 192.168.0.x 대역의 보드들은 각자 going-zpi-<끝자리>.local 이 되어 LAN 에서 자연스럽게 구분되고, mDNS 충돌도 자동 회피된다.

정리

라즈베리파이 + .NET 으로 산업 디바이스 만들 때:

다 작은 변화지만 “디바이스가 잘 동작한다” 의 느낌은 이런 디테일에서 나온다.

다음

댓글

로그인 없이 이름만 적고 남기실 수 있어요. 남긴 댓글은 바로 게시됩니다.

댓글을 불러오는 중…