Going Connect — LAN의 모든 Going 장비를 한 화면에
여러 대의 라즈베리파이 기반 장비를 깔아두면 항상 같은 문제가 옵니다.
“방금 켠 거 IP 가 뭐였더라?” “이거 1층 컨베이어용 맞아요?” “이거랑 저거 중에 어느 게 새로 받은 펌웨어죠?”
전원만 꽂으면 동작하긴 하는데, 어디에 어떤 게 떠 있는지 사람이 외워야 하는 부분이 남아 있었습니다. Going Connect 는 그 한 줄을 없애기 위해 만든 윈도우 데스크톱 앱입니다. 실행하면 같은 LAN 의 모든 Going 장비가 브랜드·제품군별로 분류돼서 한 화면에 떠 있고, 좌클릭 한 번이면 웹 UI 가 열리고, 어느 장비인지 모르겠으면 🔔 버튼을 눌러 부저를 울리게 해 물리적으로 확인할 수 있습니다.
이 글에서는 어떤 표준을 정해서 어떤 식으로 만들었는지, 그리고 만들면서 만났던 함정 몇 개를 정리합니다.
한눈에 보는 구조
PC 는 mDNS 쿼리만 보낸다. 각 장비는 표준 TXT 로 자기 정체를 광고한다.
핵심은 mDNS 입니다. mDNS 는 .local 도메인과 함께 보통 “프린터 자동 발견”용으로만 알려져 있지만, 임의의 서비스(_<type>._tcp / _<type>._udp)를 광고할 수 있는 표준이고, 라즈베리파이라면 어차피 avahi-daemon 이 항상 떠 있어서 호스트네임은 이미 광고하고 있습니다. 거기에 우리 표준 TXT 레코드 한 묶음만 얹으면 됩니다.
표준 만들기 — PROTOCOL.md
여러 제품을 한 화면에 깔끔하게 모으려면 회사가 정한 명세가 먼저 있어야 합니다. 그래서 가장 먼저 만든 게 PROTOCOL.md — Going 산하의 모든 제품이 따라야 할 mDNS 광고 규약입니다.
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.local에vendor=GoingTXT 만 광고하면 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 동작:
- 좌클릭 → 기본 브라우저로 웹 UI 열기 (caps 에
webui있을 때만) - 우클릭 → IP 복사 / 🔔 Find Me / MCP 설치 메뉴
- 🔔 Find Me →
POST /api/find-me로 부저 약 2.5초 울림 (caps 에buzzer있을 때만) - 30초마다 자동 재쿼리, 90초 미응답이면 목록에서 자동 제거
편의성 — 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 로 죽습니다.
systemd 가 Restart=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 가 webui 와 mcp 두 서비스를 광고하는데, 각각 다른 caps 를 들고 옵니다.
| 인스턴스 | caps |
|---|---|
senvas-01-web._http._tcp | webui, buzzer, display |
senvas-01-mcp._http._tcp | mcp |
트레이앱은 두 인스턴스를 같은 디바이스 한 개로 묶는데, 초기 코드가 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 외울 일 없이 작업 시작.
다음 단계
- PICO + W5500 UDP announce — mDNS 가 어려운 임베디드 라인 자동 발견
- Find Me 화면 오버레이 — 부저뿐 아니라 SENVAS Touch 화면에 풀스크린 “FIND ME · senvas-01” 5초 표시
- 자동 설치 .bat 다운로드 — 트레이앱 우클릭에서 “이 SENVAS Touch 의 MCP 등록” 한 번에
CHANGELOG 파일명에서 버전을 자동 추출하는 MSBuild 태스크, 그리고 PROTOCOL.md 표준에 맞춘 다른 제품들의 마이그레이션 같은 디테일은 다음 글에서 정리하겠습니다.
요약 — 회사가 정한 4 필수 TXT 키 + 30분이면 끝나는 펌웨어 한 벌(또는 XML 한 장). 그 뒤로는 새 제품이 LAN 에 꽂히는 순간 자동으로 화면에 떠 있습니다.
댓글
로그인 없이 이름만 적고 남기실 수 있어요. 남긴 댓글은 바로 게시됩니다.
댓글을 불러오는 중…