← 전체 글
네트워킹/약 14분 읽기/— 조회

우선순위 배열에 갇히지 않고 BACnet/IP를 SCADA로 읽어오기

SCADA가 BACnet 장치를 찾고 present-value를 읽고 쓰는 법. 객체 식별자, 16슬롯 우선순위 배열, COV 대 폴링, BBMD.

네트워킹SCADA문제 해결시운전태그

이 일에 발을 들이게 된 전화 한 통. 플랜트 SCADA가 공조기(AHU)로 급기 온도 설정값을 내려보냈고, 운전원이 값이 먹히는 걸 확인한 뒤 다들 퇴근했다. 이틀 뒤, 아침 예열을 위해 로컬 BAS 스케줄이 설정값을 올려야 하는데 아무것도 움직이지 않았다. AHU는 SCADA가 보낸 숫자를 붙들고 있었고, 로컬 컨트롤러가 아무리 써도 그 값을 밀어낼 수 없었다. 아무도 배선을 건드리지 않았다. 벌어진 일은, SCADA가 다른 누구도 이길 수 없는 우선순위로 analog-output에 명령을 걸어놓고 그것을 놓아주지 않은(relinquish) 것이다.

Modbus나 OPC UA에서 넘어온 사람들이 BACnet에서 놀라는 지점이 바로 이것이다. 값을 쓴다는 게 레지스터에 쓰는 게 아니다. 명령 가능(commandable) 객체의 경우, 16슬롯짜리 중재 배열의 한 슬롯에 값을 밀어넣는 것이고, 그 배열을 이해하지 못하면 로컬 제어와 싸우거나 로컬 제어에 지게 된다 — 겉보기엔 아무 규칙 없이.

먼저 장치를 찾아야 한다

BACnet(ASHRAE 135, ISO 16484-5)은 타이핑해 넣을 수 있는 주소를 주지 않는다. SCADA 드라이버는 Who-Is를 브로드캐스트하고 돌아오는 I-Am 응답을 모아 장치를 검색한다. 각 장치는 자신의 Device 객체 인스턴스 번호로 답하는데, 이는 0부터 4194303 사이의 값이며 전체 인터네트워크에서 유일해야 한다. IP 주소가 아니라 이 장치 인스턴스가 바인딩할 정체성이다. IP는 바뀌지만, 잘 관리된 BACnet 현장은 장치 인스턴스를 영구적인 핸들로 취급한다.

BACnet/IP는 UDP 포트 47808(0xBAC0 — 누군가 재치를 부렸다)을 탄다. 함정은 Who-Is가 브로드캐스트라는 점이고, 브로드캐스트는 IP 서브넷을 넘지 못한다. SCADA 서버가 업무망/DMZ 쪽에 있고 컨트롤러가 플랜트 VLAN에 있으면 Who-Is는 라우터에서 죽고 아무것도 검색되지 않는다. 빠져나갈 방법은 두 가지다.

  • BBMD(BACnet Broadcast Management Device) — 서브넷당 하나의 장치가 브로드캐스트 분배 테이블(BDT)을 들고 있다가 동료 BBMD로 브로드캐스트를 전달한다. 로컬 BAS에 이미 BBMD가 구성돼 있다면 대개는 항목에 당신 엔트리만 추가하면 된다.
  • Foreign Device Registration — SCADA 드라이버가 기존 BBMD에 외부 장치로 등록하되, 갱신해야 하는 TTL을 함께 건다. 컨트롤러 서브넷 밖에 사는 SCADA 박스에서 흔히 쓰는 방식이다. TTL을 합리적으로(수백 초 정도) 잡고, 만료 전에 드라이버가 재등록하는지 확인하라. 그러지 않으면 첫 만료 이후 검색이 조용히 끊긴다.

RS-485(MS/TP) 상의 필드 컨트롤러는 IP를 아예 말하지 않는다 — 네트워크 번호를 실어 나르는 BACnet 라우터 뒤에 앉아 있다. 이들도 같은 방식으로 장치 인스턴스로 읽는다. 라우팅은 라우터의 몫이지만, MS/TP 트렁크 하나가 통째로 사라지면 컨트롤러를 의심하기 전에 라우터나 네트워크 번호 충돌을 먼저 의심하라.

객체와 실제로 읽게 되는 속성

장치 안의 모든 것은 객체이고, 모든 객체는 (객체 유형, 인스턴스) 쌍으로 식별된다. 늘 다루게 되는 유형은 다음과 같다.

  • analog-input (AI) — 센서: 실내 온도, 정압, 유량.
  • analog-output (AO) — 명령 가능한 아날로그: 밸브 개도, 댐퍼, 루프로 내려보내는 설정값.
  • analog-value (AV) — 컨트롤러 안에 사는 설정값 또는 계산값.
  • binary-input / binary-output / binary-value (BI/BO/BV) — 상태와 명령.
  • multi-state-value (MSV) 계열 — Off/Low/High나 재실 모드 같은 열거 상태.
  • device — 객체 유형 8, 컨트롤러당 하나. 인스턴스 번호, 벤더 ID, 객체 목록을 담는다.

원하는 값은 거의 언제나 Present_Value 속성(속성 식별자 85)이다. 하지만 Present_Value만 읽고 그 포인트를 정상이라고 부르지 마라. 함께 Status_Flags(111)를 읽어라 — in-alarm, fault, overridden, out-of-service 네 비트다. 그 overridden 비트는 누군가(또는 무언가)가 객체를 로컬에서 명령했다는 조기 경보이고, out-of-service는 값이 물리 포인트와 완전히 분리돼 기술자가 타이핑한 무언가라는 뜻이다. out-of-service가 켜진 채로 온도가 72.0이면 그건 측정값이 아니라 남아 있는 잔재다. Status_Flags를 SCADA 태그 품질에 매핑해서, 강제(force)된 포인트가 정상으로 읽히지 않게 하라.

Units 속성(117)은 시운전 때 한 번 읽어서 캐시하라 — 이것은 열거값이고(섭씨는 62, 화씨는 64 등등), 스케일링과 라벨은 시간이 지나며 어긋나는 스프레드시트가 아니라 장치에서 나와야 한다.

이 값들을 속성 하나씩 폴링하지 마라. ReadPropertyMultiple은 여러 객체/속성 쌍을 한 요청으로 묶는데, 바쁜 MS/TP 트렁크에서 스캔이 따라잡느냐 뒤처지느냐를 가르는 차이다. 큰 객체 목록은 **세그멘테이션(segmentation)**이 필요할 수 있다. 세그멘테이션 미지원을 광고하는 장치에 큰 ReadPropertyMultiple을 보내면 abort로 실패하니, 요청을 직접 잘라서 보내야 한다.

쓰기에서는 우선순위 배열이 전부다

AHU를 문 부분이 바로 여기다. 명령 가능 객체 — AO, BO, 그리고 명령 가능한 AV/BV — 는 쓸 수 있는 단일 값이 없다. Priority_Array(속성 87)가 있다. 16개 슬롯, 우선순위 1이 가장 높고 16이 가장 낮다. Present_Value에 WriteProperty를 하면 실제로는 요청의 priority 필드가 고른 하나의 슬롯에 쓰는 것이다. 객체의 실제 Present_Value는 우선순위가 가장 높으면서 NULL이 아닌 슬롯에 있는 값이다. 모든 슬롯이 NULL이면 Relinquish_Default(속성 104)로 떨어진다.

관례(표준에서는 참고 성격이지만 널리 지켜진다)는 상위 슬롯을 안전과 수동 오버라이드에 예약한다.

  • 1 — manual-life-safety
  • 2 — automatic-life-safety
  • 8 — 수동 운전원(manual operator)
  • 16 — 가장 낮음, 흔히 로컬 프로그램의 기본 동작

운전원 설정값을 쓰는 SCADA는 보통 우선순위 8에 속한다. "확실히 먹히게" 하려고 우선순위 1이나 2에 쓴다면 — 축하한다, 이제 로컬 안전과 프로그램 로직을 눌러버린 것이고, 또 다른 우선순위 1 쓰기가 아니면 그 포인트는 움직이지 않는다. AHU가 갇힌 경위가 정확히 이것이다.

그리고 사람들이 잊는 규칙이 이것이다. 명령을 놓아주려면 당신 슬롯에 NULL을 써야 한다. 0이 아니라 NULL이다. 0.0을 쓰면 당신 슬롯에 값 0이 그대로 남는다 — 여전히 가장 높은 활성 우선순위라면 승자다. NULL을 써야만 제어를 놓아주고 다음으로 낮은 우선순위(또는 Relinquish_Default)가 넘겨받는다. 포인트에 명령을 걸고 결코 놓아주지 않는 SCADA는 누군가 배열을 수동으로 지울 때까지 그 객체를 인질로 붙든다. 놓아주기를 명령 로직에 넣어라. 운전원 명령이 "자동으로 복귀"라면, 그것은 어떤 중립적인 숫자를 쓰는 게 아니라 당신 우선순위에 NULL을 WriteProperty 하는 것이어야 한다.

갇힌 포인트를 진단할 때는 Priority_Array 전체를 읽어라. 슬롯별로 누가 어느 레벨에서 명령하고 있는지 알려준다. 열에 아홉은 몇 달 전 시운전 테스트에서 누군가 슬롯 8에 남긴 뒤 끝내 놓아주지 않은 값이 범인이다.

COV는 폴링을 이긴다 — 장치가 지원할 때

몇 초마다 모든 포인트를 폴링하는 방식은 동작하고 이해하기 쉽지만, MS/TP에서는 금방 포화된다. 변화값(Change-of-value) 구독은 이를 뒤집는다. SubscribeCOV를 보내면 Present_Value가 COV_Increment를 넘어 움직이거나 바이너리 상태가 바뀔 때 장치가 알려준다. 증분은 운전상 의미 있는 값으로 잡아라 — 실내 온도라면 0.01이 아니라 0.5 °C, 안 그러면 폴링을 괜히 복잡하게 재발명한 꼴이다.

두 가지를 제대로 해야 한다. 첫째, COV 구독에는 **수명(lifetime)**이 있다. 만료되면 장치가 구독을 버리므로, 드라이버가 그 창 안쪽에서 타이머로 재구독해야 한다. 수명을 걸어놓고 갱신하지 않으면 갱신이 조용히 멈춘다 — 장치는 멀쩡한데 화면 위 포인트만 움직임을 멈춘다. 둘째, 신뢰성이 중요한 곳에서는 confirmed COV를 선호해서 놓친 통지가 재시도되게 하라. unconfirmed는 가볍지만 손실이 있는 링크에서는 전환을 놓치고도 알지 못한다. 실용적인 패턴은, 느린 아날로그·상태 포인트 다수에는 COV를 쓰고, 놓친 통지나 조용히 만료된 구독이 남긴 낡은 값을 잡아내기 위해 느린 백그라운드 폴링(가령 몇 분마다)을 백스톱으로 함께 두는 것이다.

시운전 때 못 박아둘 것

  • 태그는 장치 인스턴스 + 객체 식별자에 바인딩하라. IP 주소에 절대 걸지 마라.
  • 쓰기 우선순위(보통 8)를 정하고 문서화하며, "자동으로 복귀"를 그 슬롯에 대한 NULL 쓰기로 만들어라.
  • BBMD 또는 foreign-device 등록이 SCADA가 실제로 사는 위치에서 동작하는지, TTL이 갱신되는지 확인하라.
  • Status_Flags(overridden, out-of-service, fault)를 태그 품질에 반영해서 강제된 포인트가 신뢰할 수 있는 것처럼 읽히지 않게 하라.
  • Units는 설계 문서가 아니라 장치에서 읽어라.
  • ReadPropertyMultiple과 COV를 선호하되, 느린 폴링을 안전망으로 남겨라.

우선순위 배열에 누군가 명령을 남겨둔 첫 포인트가, 현장 모두에게 BACnet 쓰기가 실제로 어떻게 동작하는지 가르쳐주는 포인트가 된다. 월요일 아침 더워진 건물에서 배우느니, 이 페이지에서 배우는 편이 낫다.