Going Connect — LAN의 모든 Going 장비를 한 화면에

여러 대의 라즈베리파이 기반 장비를 깔아두면 항상 같은 문제가 옵니다.

“방금 켠 거 IP 가 뭐였더라?” “이거 1층 컨베이어용 맞아요?” “이거랑 저거 중에 어느 게 새로 받은 펌웨어죠?”

전원만 꽂으면 동작하긴 하는데, 어디에 어떤 게 떠 있는지 사람이 외워야 하는 부분이 남아 있었습니다. Going Connect 는 그 한 줄을 없애기 위해 만든 윈도우 데스크톱 앱입니다. 실행하면 같은 LAN 의 모든 Going 장비가 브랜드·제품군별로 분류돼서 한 화면에 떠 있고, 좌클릭 한 번이면 웹 UI 가 열리고, 어느 장비인지 모르겠으면 🔔 버튼을 눌러 부저를 울리게 해 물리적으로 확인할 수 있습니다.

이 글에서는 어떤 표준을 정해서 어떤 식으로 만들었는지, 그리고 만들면서 만났던 함정 몇 개를 정리합니다.

한눈에 보는 구조

Going Connect 발견 아키텍처 — 윈도우 PC 가 LAN 의 mDNS 멀티캐스트로 모든 Going 장비를 잡아낸다 PC 는 mDNS 쿼리만 보낸다. 각 장비는 표준 TXT 로 자기 정체를 광고한다.

핵심은 mDNS 입니다. mDNS 는 .local 도메인과 함께 보통 “프린터 자동 발견”용으로만 알려져 있지만, 임의의 서비스(_<type>._tcp / _<type>._udp)를 광고할 수 있는 표준이고, 라즈베리파이라면 어차피 avahi-daemon 이 항상 떠 있어서 호스트네임은 이미 광고하고 있습니다. 거기에 우리 표준 TXT 레코드 한 묶음만 얹으면 됩니다.

표준 만들기 — PROTOCOL.md

여러 제품을 한 화면에 깔끔하게 모으려면 회사가 정한 명세가 먼저 있어야 합니다. 그래서 가장 먼저 만든 게 PROTOCOL.md — Going 산하의 모든 제품이 따라야 할 mDNS 광고 규약입니다.

TXT 4단 계층 — vendor / brand / family / product Going 회사 산하의 SEN 브랜드, SEN 의 SENVAS 제품군, SENVAS 의 Touch 제품. 신제품 라인이 늘어도 자동으로 분류된다.

핵심 규칙은 4 필수 TXT 키입니다.

의미
vendor회사 — Going Connect 가 1차 필터로 쓰는 키Going
product제품명SENVAS-Touch
ver펌웨어 버전1.0.0.7
serial고유 ID (MAC 끝 6자)A3B1C2

추가로 brand / family / model / dev / label / caps 같은 선택 키를 두면 트레이앱이 자동으로 브랜드별·제품군별로 그룹핑해서 보여줍니다. 신제품 펌웨어를 만들 때 따라야 하는 규칙은 딱 한 줄입니다.

Going 제품은 _http._tcp.localvendor=Going TXT 만 광고하면 Going Connect 에 자동으로 잡힙니다.

이 한 줄을 따르도록 명세를 못 박아두니, 이후에 다룬 세 가지 제품이 거의 같은 모양의 코드로 광고하게 됐고, 트레이앱 UI 도 손 안 대고 새 제품을 떠받칠 수 있게 됐습니다.

세 가지 광고 방식

같은 표준을 따르지만, 각 제품의 사정에 따라 구현하는 자리는 셋으로 갈렸습니다.

1. 펌웨어가 직접 광고 — SENVAS Touch / Going.Controller

.NET 9 Linux ARM64 펌웨어라면 Makaretu.Dns.Multicast.New 라이브러리로 직접 광고합니다. 30~40줄짜리 MdnsService 한 벌이면 끝.

var profile = new ServiceProfile($"{hostname}-web", "_http._tcp", 5000, addresses);
profile.AddProperty("vendor",  "Going");
profile.AddProperty("brand",   "SEN");
profile.AddProperty("family",  "SENVAS");
profile.AddProperty("product", "SENVAS-Touch");
profile.AddProperty("ver",     GetFirmwareVersion());
profile.AddProperty("serial",  GetSerial());     // MAC 끝 6자
profile.AddProperty("caps",    "webui,mcp,buzzer,display");

_multicast.Start();              // ← 멀티캐스트를 먼저 켜야 한다
_serviceDiscovery.Advertise(profile);
_serviceDiscovery.Announce(profile);

마지막 세 줄의 순서가 의외로 함정인데, 뒤에서 한 번 더 짚습니다.

2. 펌웨어를 손 못 대는 경우 — avahi 서비스 파일 한 장

이미 운영 중인 옛 ZPi PLC 처럼 펌웨어 빌드 환경을 다시 세팅하기 부담스러운 경우가 있습니다. 다행히 라즈베리파이의 avahi-daemon/etc/avahi/services/*.service XML 을 자동으로 읽어 광고해주기 때문에, OS 레벨에서 우회할 수 있습니다.

<?xml version="1.0" standalone='no'?>
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
  <name replace-wildcards="yes">%h-web</name>
  <service>
    <type>_http._tcp</type>
    <port>5000</port>
    <txt-record>vendor=Going</txt-record>
    <txt-record>brand=Going</txt-record>
    <txt-record>product=Going.Controller</txt-record>
    <txt-record>ver=1.0.0</txt-record>
    <txt-record>caps=webui</txt-record>
    <txt-record>role=webui</txt-record>
    <txt-record>path=/</txt-record>
  </service>
</service-group>

이 파일 한 장만 떨어트리고 sudo systemctl restart avahi-daemon 한 줄이면, 펌웨어 빌드 0번에 광고가 시작됩니다.

편의성 — 펌웨어 한 줄도 못 건드리는 현장의 옛 장비도, 라즈베리파이라면 5분 안에 Going Connect 에 잡히게 만들 수 있다.

3. Wi-Fi / OS 가 없는 임베디드 — UDP 브로드캐스트 (향후)

Arduino-Pico + W5500 같은 임베디드 보드는 mDNS responder 가 의존성이 까다로워 못 쓰는 경우가 있습니다. 이런 경우 UDP 브로드캐스트 announce 라는 가벼운 길을 둡니다. PICO 가 5초마다 LAN 으로 짧은 UDP 패킷 한 장 뿌리고, Going Connect 가 그걸 따로 수신해 mDNS 디바이스들과 같은 화면에 통합해서 표시합니다. (아직 적용 전 — 다음 글에서 자세히)

Going Connect — 윈도우 데스크톱 앱

검색 측은 .NET 9 + WPF 의 단일 인스턴스 윈도우 앱입니다. 처음엔 트레이 상주 앱으로 만들었다가, 실제 사용 흐름이 “지금 뭐가 떠 있지?” 를 자주 보고 싶은 거라 일반 윈도우로 다시 정리했습니다.

flowchart LR
    A["사용자: 새로고침"] --> B["mDNS QueryServiceInstances<br/>_http._tcp"]
    B --> C{"vendor=Going<br/>TXT 있나?"}
    C -- 없음 --> X[무시]
    C -- 있음 --> D["serial 기준으로<br/>Device 통합"]
    D --> E["Brand → Family<br/>2단 그룹화"]
    E --> F["윈도우 리스트 표시"]

핵심 UI 동작:

편의성 — IP 외울 일 0, 호스트네임 입력할 일 0. 새 장비 꽂으면 30초 안에 화면에 나타난다.

만들면서 만난 함정 셋

① mDNS Start()Announce() 순서

가장 늦게까지 찾은 함정. Makaretu.Dns 의 ServiceDiscovery.Announce() 가 내부에서 첫 광고 패킷을 멀티캐스트로 보내려고 하는데, 이때 MulticastService 가 아직 Start() 되지 않았으면 NIC 의 MTU 가 0 으로 잡혀서 ArgumentOutOfRangeException("Exceeds max packet size of 0") 가 떨어지고 프로세스가 code=killed, signal=ABRT 로 죽습니다.

systemdRestart=on-failure 로 계속 재시작 → 같은 자리에서 또 죽음 → 무한 루프. 결국 단순한 한 줄 순서 문제:

// ✗ Wrong — Start 가 뒤에 있어 Announce 가 패킷을 못 보냄
_sd.Advertise(profile);
_sd.Announce(profile);
_mdns.Start();

// ✓ Right — Multicast 먼저 켜야 NIC/MTU 가 잡혀있다
_mdns.Start();
_sd.Advertise(profile);
_sd.Announce(profile);

② WPF ContextMenu 의 RelativeSource 가 Window 를 못 찾는다

XAML 에서 디바이스 리스트 항목에 우클릭 메뉴를 달았는데, “웹 UI 열기” / “IP 복사” 메뉴가 클릭해도 무반응이었습니다. Output 창에는 6줄의 같은 에러:

System.Windows.Data Error: 4 : Cannot find source for binding with reference
'RelativeSource FindAncestor, AncestorType='System.Windows.Window'...

ContextMenu 는 메인 윈도우와 분리된 별도 popup visual tree 라서, AncestorType=Window 로 거슬러 올라가도 메인 윈도우의 DataContext 에 못 닿습니다. 표준 우회는 Border 의 Tag 에 ViewModel 을 미리 박아두고 메뉴는 PlacementTarget.Tag.XxxCommand 로 접근하는 패턴:

<Border Tag="{Binding DataContext, RelativeSource={RelativeSource AncestorType=Window}}">
  <Border.ContextMenu>
    <ContextMenu>
      <MenuItem Header="웹 UI 열기"
                Command="{Binding PlacementTarget.Tag.OpenWebCommand,
                          RelativeSource={RelativeSource AncestorType=ContextMenu}}"
                CommandParameter="{Binding PlacementTarget.DataContext,
                          RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>

③ caps 가 새로고침 때마다 사라지는 종모양

SENVAS Touch 가 webuimcp 두 서비스를 광고하는데, 각각 다른 caps 를 들고 옵니다.

인스턴스caps
senvas-01-web._http._tcpwebui, buzzer, display
senvas-01-mcp._http._tcpmcp

트레이앱은 두 인스턴스를 같은 디바이스 한 개로 묶는데, 초기 코드가 caps 를 마지막 응답으로 덮어쓰고 있었습니다. mcp 응답이 webui 응답보다 늦게 들어오면 caps = [mcp] 가 돼서 🔔 버튼이 사라집니다. 새로고침을 반복하면 응답 순서에 따라 종이 깜빡이듯 보였다 안 보였다.

수정은 한 줄 — 합집합 누적:

var incoming = txt["caps"].Split(',', ...);
device.Caps = device.Caps.Concat(incoming)
    .Distinct(StringComparer.OrdinalIgnoreCase)
    .ToArray();

한 번 본 cap 은 유지. 디바이스가 90초 동안 안 보이면 어차피 통째로 사라지고 다시 잡힐 때 새로 시작하므로, 실제로는 정상 동작합니다.

지금 상태와 다음 단계

오늘 기준 Going Connect 에 다음 제품들이 자동으로 잡힙니다.

─ SEN ▾
   └ SENVAS
      🖥 SENVAS Touch · #01 · senvas-01.local · 192.168.0.x · v1.0.0.7 🔔
─ Going ▾
   └ (Family 미정)
      🖥 Going.Controller · going-zpi.local      · 192.168.0.x · v1.0.0.0
      🖥 Going.Controller · going-zpi-XXX.local  · 192.168.0.x · v1.0.0

🔔 버튼은 SENVAS Touch (부저 탑재 모델) 에만 노출되고, 옛 PLC 와 Controller 는 좌클릭으로 웹 UI 만 바로 열립니다. 한 화면에서 IP 외울 일 없이 작업 시작.

다음 단계

CHANGELOG 파일명에서 버전을 자동 추출하는 MSBuild 태스크, 그리고 PROTOCOL.md 표준에 맞춘 다른 제품들의 마이그레이션 같은 디테일은 다음 글에서 정리하겠습니다.

요약 — 회사가 정한 4 필수 TXT 키 + 30분이면 끝나는 펌웨어 한 벌(또는 XML 한 장). 그 뒤로는 새 제품이 LAN 에 꽂히는 순간 자동으로 화면에 떠 있습니다.

댓글

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

댓글을 불러오는 중…