콘텐츠로 이동
무료 평가 시작

C 클라이언트

  • C ABI로 보안 채널과 세션을 열고 익명으로 활성화합니다.
  • UA Binary로 인코딩한 Read와 Write 요청을 보내고 응답을 해석합니다.
  • Browse로 주소 공간을 탐색하고, Call로 PDComm 시뮬레이터의 Simulator.Add 메서드를 호출합니다.
  • 실습 환경을 마친 실습 폴더: native·apps 패키지, 환경 변수, $PKI의 인증서, 빌드한 예제
  • 터미널 두 개. 각 터미널에서 환경 변수를 설정합니다.

첫 번째 터미널에서 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단계까지 그대로 둡니다.

opcua_client.c는 연결부터 종료까지 한 번에 보여 줍니다.

examples/opcua/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;
}

흐름을 단계별로 정리하면 다음과 같습니다.

  1. ABI 확인. pdcomm_opcua_abi_version이 0x00000002인지 확인합니다. 헤더와 라이브러리의 버전이 다르면 진행하지 않습니다.
  2. 탐색. pdcomm_opcua_discover가 GetEndpointsResponse 버퍼를 돌려줍니다. 예제는 ua_decode_endpoints로 엔드포인트를 출력합니다. 탐색은 인증서를 검증하지 않으므로, 이 결과로 서버를 신뢰하지 않습니다.
  3. 옵션. struct_size와 version을 채운 뒤 pdcomm_opcua_client_options_init로 기본값을 받습니다. 그다음 정책, 모드, 제한 시간, 클라이언트 인증서·개인 키, 신뢰할 서버 인증서를 넣습니다.
  4. 연결. pdcomm_opcua_client_connect가 엔드포인트를 고르고 서버 인증서를 검증한 뒤 보안 채널과 세션을 엽니다. SDK가 개인 키를 복사했으므로 바로 example_wipe로 키 버퍼를 지웁니다.
  5. 활성화. pdcomm_opcua_client_activate_anonymous가 익명 토큰으로 세션을 활성화합니다.
  6. 요청과 응답. exchange 함수가 요청 하나를 처리합니다. pdcomm_opcua_client_begin으로 요청을 넣고, pdcomm_opcua_client_poll과 pdcomm_opcua_client_take_response를 응답이 올 때까지 반복합니다. 응답 바이트는 pdcomm_opcua_buffer_copy로 두 번 호출해 복사합니다. 첫 호출로 크기를 묻고, 두 번째 호출로 복사합니다. 응답 핸들은 pdcomm_opcua_release로 해제합니다.
  7. 해석. ua_decode_write와 ua_decode_read가 서비스 결과와 항목 결과를 꺼냅니다. 둘 중 하나라도 Bad이면 종료 코드 1을 돌려줍니다.
  8. 종료. pdcomm_opcua_client_close가 CloseSession과 CloseSecureChannel을 보내고, 마지막에 클라이언트 핸들을 해제합니다.

요청 메시지는 ua_binary.h가 만듭니다. ua_encode_read와 ua_encode_write_int32는 ReadRequest(i=631)와 WriteRequest(i=673)의 인코딩 NodeId, 요청 헤더, 본문을 차례로 씁니다. 요청 헤더의 인증 토큰, 시각, 요청 핸들은 비워 둡니다. SDK가 보낼 때 채웁니다.

두 번째 터미널에서 값을 쓰고 읽습니다.

터미널 창
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=0x00000000
Read ns=2;i=1001 value=7 service=0x00000000 status=0x00000000
Session 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 1
Session activated (anonymous)
Write ns=2;i=1002 value=1 service=0x00000000 status=0x803b0000
Read ns=2;i=1002 value=100 service=0x00000000 status=0x00000000
Session 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 1003
Session activated (anonymous)
Read ns=2;i=1003 value=none service=0x00000000 status=0x801f0000
Session closed

보안 정책과 모드를 바꾸려면 --policy와 --mode를 씁니다.

터미널 창
build/opcua_client opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/server.der" --policy basic256sha256 --mode sign
Session activated (anonymous)
Read ns=2;i=1001 value=7 service=0x00000000 status=0x00000000
Session 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 오류입니다.

opcua_browse_call.c는 같은 연결 순서로 세션을 연 뒤 Browse와 Call을 보냅니다.

examples/opcua/opcua_browse_call.c
/* 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은 메서드 상태, 인자별 결과, 출력 인자를 꺼냅니다.

두 번째 터미널에서 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=0x00000000
Session closed

Server 객체 아래도 탐색합니다.

터미널 창
build/opcua_browse_call opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/server.der" --browse i=2253
Session 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=0x00000000
Session 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 2
Session 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=0x00000000
Call ns=2;s=Simulator.Add service=0x00000000 status=0x00000000 output1=3.5
Session 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.5
Session activated (anonymous)
Browse i=85
Object i=2253 "Server"
Object ns=2;s=Simulator "Simulator"
Browse service=0x00000000 status=0x00000000
Call ns=2;s=Simulator.Add service=0x00000000 status=0x80760000
Session closed

인자가 세 개이면 0x80e50000(BadTooManyArguments)입니다.

헤드리스 시뮬레이터 터미널에 다음 줄을 입력합니다. 서버를 멈추고 프로그램이 끝납니다.

{"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와 신뢰 설정을 확인합니다.