IT 제조

OPC-UA 실습 2 - PLC 테스트 코드 → OPCUA 서버 → 클라이언트(Uaexpert) 확인

keun90 2026. 1. 19. 20:05

테스트 시나리오 

  • OPCUA 서버 : Node 컨피그 작성
  • PLC 테스트 코드 작성: OPCUA로 1초 주기, 메시지 전송
  • 클라이언트(Uaexcpert) : PLC 메시지 적용 값 확인 

 

다운로드 받은 OPCUA 서버에 node 설정 값을 추가해줍니다.

  • 경로 : UaCPPServer\bin\uanodesetimport.xml 
  • 구조 Factory → Line  → PLC
<!-- Factory > Line > PLC > Message Structure -->
<UAObject NodeId="ns=1;s=Factory" BrowseName="1:Factory">
    <DisplayName>Factory</DisplayName>
    <References>
        <Reference ReferenceType="HasTypeDefinition">i=61</Reference>
        <Reference ReferenceType="Organizes" IsForward="false">i=85</Reference>
        <Reference ReferenceType="Organizes">ns=1;s=Factory.Line</Reference>
    </References>
</UAObject>
<UAObject NodeId="ns=1;s=Factory.Line" BrowseName="1:Line">
    <DisplayName>Line</DisplayName>
    <References>
        <Reference ReferenceType="HasTypeDefinition">i=61</Reference>
        <Reference ReferenceType="Organizes" IsForward="false">ns=1;s=Factory</Reference>
        <Reference ReferenceType="Organizes">ns=1;s=Factory.Line.PLC</Reference>
    </References>
</UAObject>
<UAObject NodeId="ns=1;s=Factory.Line.PLC" BrowseName="1:PLC">
    <DisplayName>PLC</DisplayName>
    <References>
        <Reference ReferenceType="HasTypeDefinition">i=61</Reference>
        <Reference ReferenceType="Organizes" IsForward="false">ns=1;s=Factory.Line</Reference>
        <Reference ReferenceType="Organizes">ns=1;s=Factory.Line.PLC.Message</Reference>
    </References>
</UAObject>
<UAVariable DataType="String" NodeId="ns=1;s=Factory.Line.PLC.Message" BrowseName="1:Message" AccessLevel="3">
    <DisplayName>Message</DisplayName>
    <References>
        <Reference ReferenceType="HasTypeDefinition">i=63</Reference>
        <Reference ReferenceType="Organizes" IsForward="false">ns=1;s=Factory.Line.PLC</Reference>
    </References>
    <Value>
        <uax:String></uax:String>
    </Value>
</UAVariable>

 

NodeId를 ns=1;로 설정해도 서버 런타임에서 NamespaceIndex가 재할당되면 변경될 수 있습니다.

저도 ns=1; 설정했지만, " ns=7;s=Factory.Line.PLC.Message " 로 설정된 것을 확인했습니다. 

PLC 메시지를 보내기 전, OPCUA 서버에서 ns 값을 조회해 후 사용(+캐싱) 하면 됩니다. 

 

 

OPCUA 서버를 다시 실행해줍니다. 

  • 실행 파일 경로 : UaCPPServer\bin\uaservercpp.exe

실행 후 uanodesetimport 값이 적용되어 어떤 NamespaceIndex 가 할당 되었는지 #결과 값에서 확인이 가능합니다. 

import asyncio
from asyncua import Client, ua

async def check_namespaces():
    async with Client("opc.tcp://keun90:48010") as client:
        ns_array = await client.get_node(
            ua.NodeId(ua.ObjectIds.Server_NamespaceArray)
        ).read_value()

        for i, uri in enumerate(ns_array):
            print(i, uri)

if __name__ == "__main__":
    asyncio.run(check_namespaces())
    
    
# 결과 값    
0 http://opcfoundation.org/UA/                                                                                          
1 urn:keun90:UnifiedAutomation:UaServerCpp                                                                                2 urn:UnifiedAutomation:PubSubConfiguration
3 http://www.unifiedautomation.com/DemoServer/                                                                          
4 urn:UnifiedAutomation:CppDemoServer:BuildingAutomation                                                                
5 http://www.unifiedautomation.com/DemoServer/AccessPermission
6 http://www.unifiedautomation.com/DemoServer/StateMachines
7 urn:UnifiedAutomation:CppDemoServer:UANodeSetXmlImport

 

 

uanodesetimport 가 정상적으로 적용되었으면, Uaexpert에서도 조회가 가능합니다.

아래와 같이 트리구조가 폴더로 보기 좋게 나타납니다.

 

 


 

OPCUA로 1초 주기, 메시지 전송하는 코드입니다.

uanodesetimport의 ns값 + Node 구조(Factory+Line+PLC)로 통신합니다. 

Message의 Value 값을 수정하면, Uaexpert에서 조회가 가능합니다.

import asyncio
import time
from asyncua import Client, ua

OPC_UA_ENDPOINT = "opc.tcp://keun90:48010"

# ✅ 서버에서 실제로 조회된 URI (ns=7에 해당)
TARGET_NAMESPACE_URI = "urn:UnifiedAutomation:CppDemoServer:UANodeSetXmlImport"

# ✅ XML에서 정의한 s=... 부분만 고정
NODE_IDENTIFIER = "Factory.Line.PLC.Message"

WRITE_INTERVAL_SEC = 1


async def resolve_node(client: Client):
    ns_array_node = client.get_node(ua.NodeId(ua.ObjectIds.Server_NamespaceArray))  # i=2255
    namespace_uris = await ns_array_node.read_value()

    ns_index = namespace_uris.index(TARGET_NAMESPACE_URI)  # 여기서 7이 나와야 정상
    node_id_str = f"ns={ns_index};s={NODE_IDENTIFIER}"
    return client.get_node(node_id_str)


async def plc_simulator():
    seq = 0

    while True:
        try:
            async with Client(OPC_UA_ENDPOINT) as client:
                node = await resolve_node(client)

                while True:
                    seq += 1
                    message = f"PLC_MSG_{seq}_{int(time.time())}"

                    dv = ua.DataValue(ua.Variant(message, ua.VariantType.String))
                    await node.write_attribute(ua.AttributeIds.Value, dv)

                    readback = await node.read_value()
                    print("WRITE:", message, "/ READBACK:", readback)

                    await asyncio.sleep(WRITE_INTERVAL_SEC)

        except Exception as e:
            print(f"[ERROR] {type(e).__name__}: {e}")
            await asyncio.sleep(2)


if __name__ == "__main__":
    asyncio.run(plc_simulator())


# plc 테스트 환경 스크립트
# python -m venv .venv
# .\.venv\Scripts\activate
# pip install asyncua
# -- 설치 확인 
# python -c "from asyncua import Client, ua; print('asyncua OK')"

 

OPCUA로 서버로 메시지 전송시 아래와 같은 에러가 발생할 수 있습니다. 

  • 노드 아이이 매칭 오류 
    packages\asyncua\ua\uatypes.py", line 383, in check raise UaStatusCodeError(self.value) asyncua.ua.uaerrors._auto.BadNodeIdUnknown: The node id refers to a node that does not exist in the server address space.
  • 쓰기 거부 오류 
    BadWriteNotSupported:
    The server does not support writing the combination of value, status and timestamps provided

 

Uaexpert에서 Value 값이 정상적으로 수정되었는지 확인해봅니다. 

 

아래는 PLC 테스트 코드에서 Value 값을 보낸 로그입니다.

 

Uaexpert의 Message.Value 값을 새로고침하면 변경되는 것을 확인할 수 있습니다.

 

 

 

다음 단계입니다.

- PLC 테스트 코드를 하나 더 증가

- Message Value 단순 구조가 아닌, 실제 Message 포맷으로 변경 

- 메시지 스트레스 테스트, 가능할까..

- Pub/Sub 구조 

- OPCUA 고급 설정