C 클라이언트
- C ABI로 보안 채널과 세션을 열고 익명으로 활성화합니다.
- UA Binary로 인코딩한 Read와 Write 요청을 보내고 응답을 해석합니다.
- Browse로 주소 공간을 탐색하고, Call로 PDComm 시뮬레이터의
Simulator.Add메서드를 호출합니다.
준비물
섹션 제목: “준비물”- 실습 환경을 마친 실습 폴더:
native·apps패키지, 환경 변수,$PKI의 인증서, 빌드한 예제 - 터미널 두 개. 각 터미널에서 환경 변수를 설정합니다.
1. 예제 서버 실행
섹션 제목: “1. 예제 서버 실행”첫 번째 터미널에서 SDK 예제 서버를 실행합니다.
cd "$OPCUA_SDK"build/opcua_server 127.0.0.1:4840 opc.tcp://localhost:4840/pdcomm "$PKI/server.der" "$PKI/server.key.pem" "$PKI/client.der"Listening on 127.0.0.1:4840이 터미널은 5단계까지 그대로 둡니다.
2. 클라이언트 코드 읽기
섹션 제목: “2. 클라이언트 코드 읽기”opcua_client.c는 연결부터 종료까지 한 번에 보여 줍니다.
/* PDComm OPC UA C client example. Discovers the server endpoints, opens a secure session, optionally writes an Int32 value and reads it back. Exit codes: 0 success, 1 a Bad OPC UA status, 2 invalid arguments, 3 SDK/runtime failure. Credentials are never printed. */#ifndef _WIN32#define _POSIX_C_SOURCE 200809L#endif#include <pdcomm/opcua/pdcomm_opcua.h>#include <stdint.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include "example_io.h"#include "ua_binary.h"
static const char *USAGE = "Usage: opcua_client ENDPOINT CLIENT_CERT_DER CLIENT_KEY_PEM TRUSTED_SERVER_DER [options]\n" " --policy aes256sha256rsapss|aes128sha256rsaoaep|basic256sha256|none\n" " --mode signandencrypt|sign|none (none requires --policy none)\n" " --node ID numeric identifier in namespace 2 (default 1001)\n" " --write VALUE write an Int32 value before reading it\n" " --timeout MS network timeout in milliseconds (default 5000)\n";
static void print_error(const char *operation, int32_t status) { uint8_t text[512]; size_t required = 0; if (pdcomm_opcua_last_error_copy(text, sizeof(text) - 1, &required) != PDCOMM_OPCUA_OK) required = 0; text[required < sizeof(text) ? required : sizeof(text) - 1] = 0; fprintf(stderr, "Error: %s failed (SDK status %d): %s\n", operation, (int)status, required ? (const char *)text : "no detail");}
static int parse_u32(const char *text, uint32_t minimum, uint32_t maximum, uint32_t *out) { char *end = NULL; unsigned long value = strtoul(text, &end, 10); if (!*text || *end || value < minimum || value > maximum) return -1; *out = (uint32_t)value; return 0;}
static void show_endpoint(ua_text url, uint32_t mode, ua_text policy, void *context) { static const char *modes[] = {"Invalid", "None", "Sign", "SignAndEncrypt"}; (void)context; printf("Endpoint %.*s mode=%s policy=%.*s\n", (int)url.size, url.data ? (const char *)url.data : "", mode < 4 ? modes[mode] : "Unknown", (int)policy.size, policy.data ? (const char *)policy.data : "");}
/* Sends one request and polls until its response is available. */static int32_t exchange(pdcomm_opcua_handle client, const uint8_t *request, size_t size, uint32_t timeout_ms, uint8_t **body, size_t *body_size) { uint32_t id = 0, waited = 0; pdcomm_opcua_handle response = 0; size_t required = 0; int32_t status = pdcomm_opcua_client_begin(client, request, size, &id); *body = NULL; *body_size = 0; if (status != PDCOMM_OPCUA_OK) return status; for (;;) { status = pdcomm_opcua_client_poll(client); if (status != PDCOMM_OPCUA_OK) return status; status = pdcomm_opcua_client_take_response(client, id, &response); if (status == PDCOMM_OPCUA_OK) break; if (status != PDCOMM_OPCUA_EMPTY) return status; if (waited++ >= timeout_ms) return PDCOMM_OPCUA_RUNTIME_ERROR; example_sleep_ms(1); } status = pdcomm_opcua_buffer_copy(response, NULL, 0, &required); if (status == PDCOMM_OPCUA_BUFFER_TOO_SMALL && required > 0) { *body = (uint8_t *)malloc(required); status = *body ? pdcomm_opcua_buffer_copy(response, *body, required, body_size) : PDCOMM_OPCUA_INTERNAL_ERROR; } pdcomm_opcua_release(response); return status;}
int main(int argc, char **argv) { pdcomm_opcua_client_options options; pdcomm_opcua_handle client = 0, endpoints = 0; unsigned char *certificate = NULL, *key = NULL, *trust = NULL; size_t certificate_size = 0, key_size = 0, trust_size = 0, size; uint32_t policy = PDCOMM_OPCUA_AES256SHA256RSAPSS, mode = PDCOMM_OPCUA_SIGN_AND_ENCRYPT; uint32_t node = 1001, timeout = 5000, service_result = 0, item_status = 0; int32_t status, write_value = 0; int write = 0, result = 3, i, mode_set = 0; uint8_t request[256], *body = NULL; ua_value value;
if (argc < 5 || (argc - 5) % 2) { fputs(USAGE, stderr); return 2; } for (i = 5; i < argc; i += 2) { const char *name = argv[i], *text = argv[i + 1]; if (!strcmp(name, "--policy")) { if (!strcmp(text, "aes256sha256rsapss")) policy = PDCOMM_OPCUA_AES256SHA256RSAPSS; else if (!strcmp(text, "aes128sha256rsaoaep")) policy = PDCOMM_OPCUA_AES128SHA256RSAOAEP; else if (!strcmp(text, "basic256sha256")) policy = PDCOMM_OPCUA_BASIC256SHA256; else if (!strcmp(text, "none")) policy = PDCOMM_OPCUA_POLICY_NONE; else { fputs(USAGE, stderr); return 2; } } else if (!strcmp(name, "--mode")) { mode_set = 1; if (!strcmp(text, "signandencrypt")) mode = PDCOMM_OPCUA_SIGN_AND_ENCRYPT; else if (!strcmp(text, "sign")) mode = PDCOMM_OPCUA_SIGN; else if (!strcmp(text, "none")) mode = PDCOMM_OPCUA_MODE_NONE; else { fputs(USAGE, stderr); return 2; } } else if (!strcmp(name, "--node")) { if (parse_u32(text, 1, 0xffffffffu, &node)) { fputs(USAGE, stderr); return 2; } } else if (!strcmp(name, "--timeout")) { if (parse_u32(text, 1, 600000, &timeout)) { fputs(USAGE, stderr); return 2; } } else if (!strcmp(name, "--write")) { char *end = NULL; long long parsed = strtoll(text, &end, 10); if (!*text || *end || parsed < INT32_MIN || parsed > INT32_MAX) { fputs(USAGE, stderr); return 2; } write = 1; write_value = (int32_t)parsed; } else { fputs(USAGE, stderr); return 2; } } if (policy == PDCOMM_OPCUA_POLICY_NONE && !mode_set) mode = PDCOMM_OPCUA_MODE_NONE; if ((policy == PDCOMM_OPCUA_POLICY_NONE) != (mode == PDCOMM_OPCUA_MODE_NONE)) { fputs("Error: --policy none and --mode none must be used together\n", stderr); return 2; } if (pdcomm_opcua_abi_version() != 0x00000002u) { fputs("Error: unsupported PDComm OPC UA ABI version\n", stderr); return 3; } printf("PDComm OPC UA ABI 0x%08x\n", (unsigned)pdcomm_opcua_abi_version());
/* Discovery is unauthenticated and returns a GetEndpointsResponse. */ status = pdcomm_opcua_discover((const uint8_t *)argv[1], strlen(argv[1]), timeout, &endpoints); if (status != PDCOMM_OPCUA_OK) { print_error("discover", status); return 3; } status = pdcomm_opcua_buffer_copy(endpoints, NULL, 0, &size); if (status == PDCOMM_OPCUA_BUFFER_TOO_SMALL && (body = (uint8_t *)malloc(size)) != NULL) status = pdcomm_opcua_buffer_copy(endpoints, body, size, &size); pdcomm_opcua_release(endpoints); if (status != PDCOMM_OPCUA_OK || !body || ua_decode_endpoints(body, size, show_endpoint, NULL) < 0) { free(body); fputs("Error: invalid GetEndpoints response\n", stderr); return 3; } free(body); body = NULL;
if (example_load(argv[2], &certificate, &certificate_size) || example_load(argv[3], &key, &key_size) || example_load(argv[4], &trust, &trust_size)) { fputs("Error: cannot read the certificate, private key or trusted server certificate\n", stderr); goto done; } memset(&options, 0, sizeof(options)); options.struct_size = (uint32_t)sizeof(options); options.version = 1; status = pdcomm_opcua_client_options_init(&options); if (status != PDCOMM_OPCUA_OK) { print_error("options", status); goto done; } options.timeout_ms = timeout; options.policy = policy; options.mode = mode; options.flags = policy == PDCOMM_OPCUA_POLICY_NONE ? PDCOMM_OPCUA_ALLOW_NONE : 0; options.certificate_der.data = certificate; options.certificate_der.size = certificate_size; options.private_key_pem.data = key; options.private_key_pem.size = key_size; options.trusted_server_der.data = trust; options.trusted_server_der.size = trust_size; status = pdcomm_opcua_client_connect((const uint8_t *)argv[1], strlen(argv[1]), &options, &client); /* The SDK copied the identity; wipe the private key now. */ example_wipe(key, key_size); key = NULL; if (status != PDCOMM_OPCUA_OK) { print_error("connect", status); goto done; } status = pdcomm_opcua_client_activate_anonymous(client); if (status != PDCOMM_OPCUA_OK) { print_error("activate", status); goto done; } printf("Session activated (anonymous)\n");
result = 0; if (write) { size = ua_encode_write_int32(request, sizeof(request), 2, node, write_value); status = exchange(client, request, size, timeout, &body, &size); if (status != PDCOMM_OPCUA_OK) { print_error("write", status); result = 3; goto done; } if (ua_decode_write(body, size, &service_result, &item_status)) { fputs("Error: malformed WriteResponse\n", stderr); result = 3; goto done; } printf("Write ns=2;i=%u value=%d service=0x%08x status=0x%08x\n", (unsigned)node, (int)write_value, (unsigned)service_result, (unsigned)item_status); if ((service_result | item_status) & 0x80000000u) result = 1; free(body); body = NULL; } size = ua_encode_read(request, sizeof(request), 2, node); status = exchange(client, request, size, timeout, &body, &size); if (status != PDCOMM_OPCUA_OK) { print_error("read", status); result = 3; goto done; } if (ua_decode_read(body, size, &service_result, &value)) { fputs("Error: malformed ReadResponse\n", stderr); result = 3; goto done; } if (value.has_int32) printf("Read ns=2;i=%u value=%d service=0x%08x status=0x%08x\n", (unsigned)node, (int)value.int32, (unsigned)service_result, (unsigned)value.status); else printf("Read ns=2;i=%u value=none service=0x%08x status=0x%08x\n", (unsigned)node, (unsigned)service_result, (unsigned)value.status); if ((service_result | value.status) & 0x80000000u) result = 1; status = pdcomm_opcua_client_close(client); if (status != PDCOMM_OPCUA_OK) { print_error("close", status); result = 3; goto done; } printf("Session closed\n");
done: free(body); if (client) pdcomm_opcua_release(client); if (key) example_wipe(key, key_size); free(certificate); free(trust); return result;}흐름을 단계별로 정리하면 다음과 같습니다.
- ABI 확인.
pdcomm_opcua_abi_version이0x00000002인지 확인합니다. 헤더와 라이브러리의 버전이 다르면 진행하지 않습니다. - 탐색.
pdcomm_opcua_discover가 GetEndpointsResponse 버퍼를 돌려줍니다. 예제는ua_decode_endpoints로 엔드포인트를 출력합니다. 탐색은 인증서를 검증하지 않으므로, 이 결과로 서버를 신뢰하지 않습니다. - 옵션.
struct_size와version을 채운 뒤pdcomm_opcua_client_options_init로 기본값을 받습니다. 그다음 정책, 모드, 제한 시간, 클라이언트 인증서·개인 키, 신뢰할 서버 인증서를 넣습니다. - 연결.
pdcomm_opcua_client_connect가 엔드포인트를 고르고 서버 인증서를 검증한 뒤 보안 채널과 세션을 엽니다. SDK가 개인 키를 복사했으므로 바로example_wipe로 키 버퍼를 지웁니다. - 활성화.
pdcomm_opcua_client_activate_anonymous가 익명 토큰으로 세션을 활성화합니다. - 요청과 응답.
exchange함수가 요청 하나를 처리합니다.pdcomm_opcua_client_begin으로 요청을 넣고,pdcomm_opcua_client_poll과pdcomm_opcua_client_take_response를 응답이 올 때까지 반복합니다. 응답 바이트는pdcomm_opcua_buffer_copy로 두 번 호출해 복사합니다. 첫 호출로 크기를 묻고, 두 번째 호출로 복사합니다. 응답 핸들은pdcomm_opcua_release로 해제합니다. - 해석.
ua_decode_write와ua_decode_read가 서비스 결과와 항목 결과를 꺼냅니다. 둘 중 하나라도 Bad이면 종료 코드 1을 돌려줍니다. - 종료.
pdcomm_opcua_client_close가 CloseSession과 CloseSecureChannel을 보내고, 마지막에 클라이언트 핸들을 해제합니다.
요청 메시지는 ua_binary.h가 만듭니다. ua_encode_read와 ua_encode_write_int32는 ReadRequest(i=631)와 WriteRequest(i=673)의 인코딩 NodeId, 요청 헤더, 본문을 차례로 씁니다. 요청 헤더의 인증 토큰, 시각, 요청 핸들은 비워 둡니다. SDK가 보낼 때 채웁니다.
3. 읽기와 쓰기
섹션 제목: “3. 읽기와 쓰기”두 번째 터미널에서 값을 쓰고 읽습니다.
cd "$OPCUA_SDK"build/opcua_client opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/server.der" --write 7엔드포인트 목록 뒤의 출력은 다음과 같습니다.
Session activated (anonymous)Write ns=2;i=1001 value=7 service=0x00000000 status=0x00000000Read ns=2;i=1001 value=7 service=0x00000000 status=0x00000000Session closed읽기 전용 변수 ns=2;i=1002에 써 봅니다.
build/opcua_client opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/server.der" --node 1002 --write 1Session activated (anonymous)Write ns=2;i=1002 value=1 service=0x00000000 status=0x803b0000Read ns=2;i=1002 value=100 service=0x00000000 status=0x00000000Session closed서비스 결과는 Good이고 항목 결과만 0x803b0000(BadNotWritable)입니다. 값은 100 그대로이고 종료 코드는 1입니다.
익명 세션으로 인증 사용자 전용 변수 ns=2;i=1003을 읽으면 0x801f0000(BadUserAccessDenied)입니다.
build/opcua_client opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/server.der" --node 1003Session activated (anonymous)Read ns=2;i=1003 value=none service=0x00000000 status=0x801f0000Session closed보안 정책과 모드를 바꾸려면 --policy와 --mode를 씁니다.
build/opcua_client opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/server.der" --policy basic256sha256 --mode signSession activated (anonymous)Read ns=2;i=1001 value=7 service=0x00000000 status=0x00000000Session closed| 옵션 | 설명 |
|---|---|
--policy aes256sha256rsapss, aes128sha256rsaoaep, basic256sha256, none |
보안 정책(기본값 Aes256_Sha256_RsaPss) |
--mode signandencrypt, sign, none |
보안 모드(기본값 SignAndEncrypt). none은 --policy none과 함께 씁니다. |
--node ID |
네임스페이스 2의 숫자 식별자(기본값 1001) |
--write VALUE |
읽기 전에 Int32 값을 씁니다. |
--timeout MS |
네트워크 제한 시간(기본값 5000 ms) |
종료 코드는 0 성공, 1 Bad StatusCode 수신, 2 잘못된 인자, 3 연결 또는 SDK 오류입니다.
4. Browse 코드 읽기
섹션 제목: “4. Browse 코드 읽기”opcua_browse_call.c는 같은 연결 순서로 세션을 연 뒤 Browse와 Call을 보냅니다.
/* PDComm OPC UA C browse and call example. Opens a secure anonymous session, lists the forward hierarchical references of a node (Browse) and optionally calls a method with Double arguments (Call). Exit codes: 0 success, 1 a Bad OPC UA status, 2 invalid arguments, 3 SDK/runtime failure. Credentials are never printed. */#ifndef _WIN32#define _POSIX_C_SOURCE 200809L#endif#include <pdcomm/opcua/pdcomm_opcua.h>#include <stdint.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include "example_io.h"#include "ua_binary.h"
#define MAX_ARGUMENTS 8u
static const char *USAGE = "Usage: opcua_browse_call ENDPOINT CLIENT_CERT_DER CLIENT_KEY_PEM TRUSTED_SERVER_DER [options]\n" " --browse NODEID node whose references are listed (default i=85, the Objects folder)\n" " --method NODEID method to call after browsing, for example ns=2;s=Simulator.Add\n" " --object NODEID object that owns the method (default ns=2;s=Simulator)\n" " --arg VALUE Double input argument; repeat it for each argument (at most 8)\n" " --policy aes256sha256rsapss|aes128sha256rsaoaep|basic256sha256\n" " --mode signandencrypt|sign\n" " --timeout MS network timeout in milliseconds (default 5000)\n";
static const char *node_class(uint32_t value) { switch (value) { case 1: return "Object"; case 2: return "Variable"; case 4: return "Method"; case 8: return "ObjectType"; case 16: return "VariableType"; case 32: return "ReferenceType"; case 64: return "DataType"; case 128: return "View"; default: return "Unspecified"; }}
/* Writes "ns=2;s=Name", "i=85" or "ns=1;(opaque)" for printing. */static void node_text(const ua_node *node, char *out, size_t capacity) { char prefix[16] = ""; if (node->ns) snprintf(prefix, sizeof(prefix), "ns=%u;", (unsigned)node->ns); if (node->kind == UA_NODE_STRING) snprintf(out, capacity, "%ss=%.*s", prefix, (int)(node->string.size > 200 ? 200 : node->string.size), node->string.data ? (const char *)node->string.data : ""); else if (node->kind == UA_NODE_NUMERIC) snprintf(out, capacity, "%si=%lu", prefix, (unsigned long)node->numeric); else snprintf(out, capacity, "%s(opaque)", prefix);}
static void show_reference(const ua_reference *reference, void *context) { char target[256]; (void)context; node_text(&reference->node, target, sizeof(target)); printf(" %-8s %s%s \"%.*s\"\n", node_class(reference->node_class), target, reference->remote ? " (remote)" : "", (int)(reference->browse_name.size > 200 ? 200 : reference->browse_name.size), reference->browse_name.data ? (const char *)reference->browse_name.data : "");}
static void print_error(const char *operation, int32_t status) { uint8_t text[512]; size_t required = 0; if (pdcomm_opcua_last_error_copy(text, sizeof(text) - 1, &required) != PDCOMM_OPCUA_OK) required = 0; text[required < sizeof(text) ? required : sizeof(text) - 1] = 0; fprintf(stderr, "Error: %s failed (SDK status %d): %s\n", operation, (int)status, required ? (const char *)text : "no detail");}
/* Sends one request and polls until its response is available. */static int32_t exchange(pdcomm_opcua_handle client, const uint8_t *request, size_t size, uint32_t timeout_ms, uint8_t **body, size_t *body_size) { uint32_t id = 0, waited = 0; pdcomm_opcua_handle response = 0; size_t required = 0; int32_t status = pdcomm_opcua_client_begin(client, request, size, &id); *body = NULL; *body_size = 0; if (status != PDCOMM_OPCUA_OK) return status; for (;;) { status = pdcomm_opcua_client_poll(client); if (status != PDCOMM_OPCUA_OK) return status; status = pdcomm_opcua_client_take_response(client, id, &response); if (status == PDCOMM_OPCUA_OK) break; if (status != PDCOMM_OPCUA_EMPTY) return status; if (waited++ >= timeout_ms) return PDCOMM_OPCUA_RUNTIME_ERROR; example_sleep_ms(1); } status = pdcomm_opcua_buffer_copy(response, NULL, 0, &required); if (status == PDCOMM_OPCUA_BUFFER_TOO_SMALL && required > 0) { *body = (uint8_t *)malloc(required); status = *body ? pdcomm_opcua_buffer_copy(response, *body, required, body_size) : PDCOMM_OPCUA_INTERNAL_ERROR; } pdcomm_opcua_release(response); return status;}
int main(int argc, char **argv) { pdcomm_opcua_client_options options; pdcomm_opcua_handle client = 0; unsigned char *certificate = NULL, *key = NULL, *trust = NULL; size_t certificate_size = 0, key_size = 0, trust_size = 0, size = 0; uint32_t policy = PDCOMM_OPCUA_AES256SHA256RSAPSS, mode = PDCOMM_OPCUA_SIGN_AND_ENCRYPT, timeout = 5000; uint32_t service_result = 0, status_code = 0, inputs[MAX_ARGUMENTS], input_count = 0, output_count = 0, i; uint32_t argument_count = 0; double arguments[MAX_ARGUMENTS]; ua_number outputs[MAX_ARGUMENTS]; const char *browse_text = "i=85", *object_text = "ns=2;s=Simulator", *method_text = NULL; ua_node browse_node, object_node, method_node; uint8_t request[8192], *body = NULL; int32_t status; int result = 3, more = 0, a;
if (argc < 5 || (argc - 5) % 2) { fputs(USAGE, stderr); return 2; } for (a = 5; a < argc; a += 2) { const char *name = argv[a], *text = argv[a + 1]; char *end = NULL; if (!strcmp(name, "--browse")) browse_text = text; else if (!strcmp(name, "--object")) object_text = text; else if (!strcmp(name, "--method")) method_text = text; else if (!strcmp(name, "--arg")) { if (argument_count == MAX_ARGUMENTS) { fputs(USAGE, stderr); return 2; } arguments[argument_count] = strtod(text, &end); if (!*text || *end) { fputs(USAGE, stderr); return 2; } ++argument_count; } else if (!strcmp(name, "--policy")) { if (!strcmp(text, "aes256sha256rsapss")) policy = PDCOMM_OPCUA_AES256SHA256RSAPSS; else if (!strcmp(text, "aes128sha256rsaoaep")) policy = PDCOMM_OPCUA_AES128SHA256RSAOAEP; else if (!strcmp(text, "basic256sha256")) policy = PDCOMM_OPCUA_BASIC256SHA256; else { fputs(USAGE, stderr); return 2; } } else if (!strcmp(name, "--mode")) { if (!strcmp(text, "signandencrypt")) mode = PDCOMM_OPCUA_SIGN_AND_ENCRYPT; else if (!strcmp(text, "sign")) mode = PDCOMM_OPCUA_SIGN; else { fputs(USAGE, stderr); return 2; } } else if (!strcmp(name, "--timeout")) { unsigned long value = strtoul(text, &end, 10); if (!*text || *end || value < 1 || value > 600000) { fputs(USAGE, stderr); return 2; } timeout = (uint32_t)value; } else { fputs(USAGE, stderr); return 2; } } if (ua_parse_node(browse_text, &browse_node) || ua_parse_node(object_text, &object_node) || (method_text && ua_parse_node(method_text, &method_node))) { fputs("Error: a NodeId must look like i=85, ns=2;i=1001 or ns=2;s=Name\n", stderr); return 2; } if (pdcomm_opcua_abi_version() != 0x00000002u) { fputs("Error: unsupported PDComm OPC UA ABI version\n", stderr); return 3; }
if (example_load(argv[2], &certificate, &certificate_size) || example_load(argv[3], &key, &key_size) || example_load(argv[4], &trust, &trust_size)) { fputs("Error: cannot read the certificate, private key or trusted server certificate\n", stderr); goto done; } memset(&options, 0, sizeof(options)); options.struct_size = (uint32_t)sizeof(options); options.version = 1; status = pdcomm_opcua_client_options_init(&options); if (status != PDCOMM_OPCUA_OK) { print_error("options", status); goto done; } options.timeout_ms = timeout; options.policy = policy; options.mode = mode; options.certificate_der.data = certificate; options.certificate_der.size = certificate_size; options.private_key_pem.data = key; options.private_key_pem.size = key_size; options.trusted_server_der.data = trust; options.trusted_server_der.size = trust_size; status = pdcomm_opcua_client_connect((const uint8_t *)argv[1], strlen(argv[1]), &options, &client); /* The SDK copied the identity; wipe the private key now. */ example_wipe(key, key_size); key = NULL; if (status != PDCOMM_OPCUA_OK) { print_error("connect", status); goto done; } status = pdcomm_opcua_client_activate_anonymous(client); if (status != PDCOMM_OPCUA_OK) { print_error("activate", status); goto done; } printf("Session activated (anonymous)\n");
/* Browse: forward hierarchical references, at most 100 per call. */ result = 0; size = ua_encode_browse(request, sizeof(request), &browse_node, 100); status = size ? exchange(client, request, size, timeout, &body, &size) : PDCOMM_OPCUA_INVALID_INPUT; if (status != PDCOMM_OPCUA_OK) { print_error("browse", status); result = 3; goto done; } printf("Browse %s\n", browse_text); if (ua_decode_browse(body, size, &service_result, &status_code, &more, show_reference, NULL)) { fputs("Error: malformed BrowseResponse\n", stderr); result = 3; goto done; } printf("Browse service=0x%08x status=0x%08x%s\n", (unsigned)service_result, (unsigned)status_code, more ? " (more references: continue with BrowseNext)" : ""); if ((service_result | status_code) & 0x80000000u) result = 1; free(body); body = NULL;
/* Call: one method with the Double input arguments from the command line. */ if (method_text) { size = ua_encode_call(request, sizeof(request), &object_node, &method_node, arguments, argument_count); status = size ? exchange(client, request, size, timeout, &body, &size) : PDCOMM_OPCUA_INVALID_INPUT; if (status != PDCOMM_OPCUA_OK) { print_error("call", status); result = 3; goto done; } if (ua_decode_call(body, size, &service_result, &status_code, inputs, &input_count, outputs, &output_count, MAX_ARGUMENTS)) { fputs("Error: malformed CallResponse or an output type this example does not decode\n", stderr); result = 3; goto done; } printf("Call %s service=0x%08x status=0x%08x", method_text, (unsigned)service_result, (unsigned)status_code); for (i = 0; i < input_count; ++i) if (inputs[i] & 0x80000000u) printf(" input%u=0x%08x", (unsigned)i + 1, (unsigned)inputs[i]); for (i = 0; i < output_count; ++i) printf(" output%u=%.17g", (unsigned)i + 1, outputs[i].value); printf("\n"); if ((service_result | status_code) & 0x80000000u) result = 1; } status = pdcomm_opcua_client_close(client); if (status != PDCOMM_OPCUA_OK) { print_error("close", status); result = 3; goto done; } printf("Session closed\n");
done: free(body); if (client) pdcomm_opcua_release(client); if (key) example_wipe(key, key_size); free(certificate); free(trust); return result;}ua_parse_node는i=85,ns=2;i=1001,ns=2;s=Simulator.Add형식의 NodeId 텍스트를 읽습니다.ua_encode_browse는 BrowseRequest(i=527)를 만듭니다. 순방향, HierarchicalReferences(i=33)와 그 하위 타입, 모든 NodeClass, 모든 결과 필드를 요청합니다. 노드당 참조는 100개까지 받습니다.ua_decode_browse는 참조마다show_reference를 부릅니다. 출력의 첫 열은 NodeClass, 둘째 열은 대상 NodeId, 셋째 열은 BrowseName입니다. 참조가 더 남으면 BrowseNext가 필요하다고 표시합니다.ua_encode_call은 CallRequest(i=712)를 만듭니다. 입력 인자는 Double Variant입니다.ua_decode_call은 메서드 상태, 인자별 결과, 출력 인자를 꺼냅니다.
5. 예제 서버 탐색
섹션 제목: “5. 예제 서버 탐색”두 번째 터미널에서 Objects 폴더(i=85)를 탐색합니다.
build/opcua_browse_call opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/server.der"Session activated (anonymous)Browse i=85 Object i=2253 "Server" Variable ns=2;i=1001 "Value" Variable ns=2;i=1002 "Limit" Variable ns=2;i=1003 "Protected"Browse service=0x00000000 status=0x00000000Session closedServer 객체 아래도 탐색합니다.
build/opcua_browse_call opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/server.der" --browse i=2253Session activated (anonymous)Browse i=2253 Object i=2268 "ServerCapabilities" Object i=2295 "VendorServerInfo" Object i=2296 "ServerRedundancy" Variable i=2256 "ServerStatus" Variable i=2254 "ServerArray" Variable i=2255 "NamespaceArray" Variable i=2267 "ServiceLevel" Variable i=2994 "Auditing" Object i=2274 "ServerDiagnostics"Browse service=0x00000000 status=0x00000000Session closed예제 서버에는 메서드가 없습니다. 첫 번째 터미널에서 Ctrl+C를 눌러 서버를 끝냅니다.
6. 시뮬레이터 서버에서 메서드 호출
섹션 제목: “6. 시뮬레이터 서버에서 메서드 호출”PDComm 시뮬레이터 서버에는 Simulator.Add 메서드가 있습니다. 첫 번째 터미널에서 헤드리스 시뮬레이터를 $PKI 폴더에서 실행합니다.
cd "$PKI""$OPCUA_APPS/bin/pdcomm-opcua-headless"프로그램은 표준 입력에서 JSON 명령을 한 줄씩 읽습니다. 다음 줄을 입력해 서버의 주소와 인증서를 설정합니다. 파일 이름은 현재 폴더 기준입니다.
{"action":"configure","config":{"server":{"address":"127.0.0.1:4841","certificate":"server.cert.pem","private_key":"server.key.pem","trust":["client.der"]}}}응답으로 전체 구성이 한 줄로 나옵니다. 이어서 서버를 시작합니다.
{"action":"server_start"}{"ok":true,"result":{"audit":[],"endpoint":"opc.tcp://localhost:4841/pdcomm/simulator","error":null,"running":true,"secure":true,"sessions":0}}두 번째 터미널에서 Simulator 객체를 탐색하고 Simulator.Add를 1.5와 2로 호출합니다. ;가 들어간 NodeId는 따옴표로 감쌉니다.
build/opcua_browse_call opc.tcp://localhost:4841/pdcomm/simulator "$PKI/client.der" "$PKI/client.key.pem" "$PKI/server.der" \ --browse 'ns=2;s=Simulator' --method 'ns=2;s=Simulator.Add' --arg 1.5 --arg 2Session activated (anonymous)Browse ns=2;s=Simulator Variable ns=2;s=Simulator.Boolean "Simulator.Boolean" Variable ns=2;s=Simulator.Int32 "Simulator.Int32" Variable ns=2;s=Simulator.Double "Simulator.Double" Variable ns=2;s=Simulator.String "Simulator.String" Variable ns=2;s=Simulator.Counter "Simulator.Counter" Variable ns=2;s=Simulator.Protected "Simulator.Protected" Variable ns=2;s=Simulator.AlarmSource "Simulator.AlarmSource" Method ns=2;s=Simulator.Add "Simulator.Add" Method ns=2;s=Simulator.Delay "Simulator.Delay"Browse service=0x00000000 status=0x00000000Call ns=2;s=Simulator.Add service=0x00000000 status=0x00000000 output1=3.5Session closed--object를 지정하지 않으면 메서드의 소유 객체로 ns=2;s=Simulator를 씁니다. 인자를 하나만 주면 메서드가 0x80760000(BadArgumentsMissing)을 돌려주고 종료 코드는 1입니다.
build/opcua_browse_call opc.tcp://localhost:4841/pdcomm/simulator "$PKI/client.der" "$PKI/client.key.pem" "$PKI/server.der" \ --method 'ns=2;s=Simulator.Add' --arg 1.5Session activated (anonymous)Browse i=85 Object i=2253 "Server" Object ns=2;s=Simulator "Simulator"Browse service=0x00000000 status=0x00000000Call ns=2;s=Simulator.Add service=0x00000000 status=0x80760000Session closed인자가 세 개이면 0x80e50000(BadTooManyArguments)입니다.
7. 정리
섹션 제목: “7. 정리”헤드리스 시뮬레이터 터미널에 다음 줄을 입력합니다. 서버를 멈추고 프로그램이 끝납니다.
{"action":"shutdown"}{"ok":true,"result":{"client_closed":null,"pubsub_stopped":false,"server_stopped":true}}자주 생기는 오류
섹션 제목: “자주 생기는 오류”| 증상 | 원인과 조치 |
|---|---|
Error: a NodeId must look like i=85, ns=2;i=1001 or ns=2;s=Name |
셸이 ;에서 명령을 나눴습니다. 'ns=2;s=Simulator'처럼 따옴표로 감쌉니다. |
Error: discover failed (SDK status 5): OPC UA status: 0x80830000 |
URL의 경로가 서버와 다릅니다(BadTcpEndpointUrlInvalid). 시뮬레이터 경로는 /pdcomm/simulator입니다. |
Error: connect failed (SDK status 5): Requested OPC UA endpoint not advertised |
서버가 요청한 정책과 모드의 엔드포인트를 광고하지 않습니다. 예제 서버는 None을 광고하지 않으므로 --policy none은 거부됩니다. |
Error: connect failed (SDK status 5): OPC UA security CertificateUntrusted |
신뢰할 서버 인증서가 서버의 실제 인증서와 다릅니다. PKI와 신뢰 설정을 확인합니다. |