본문으로 건너뛰기

태그 관리 API

개요

태그 관리 API는 OT 장비의 데이터 포인트(태그)를 읽고 쓰며 실시간으로 변경 사항을 구독하는 기능을 제공합니다. gRPC 서비스 CaffeineApi를 통해 REST API로도 사용 가능합니다.

기본 정보

항목
서비스CaffeineApi
엔드포인트Engine gRPC 포트 (기본: 5001)
인증JWT 토큰 필요 (WriteTag, WriteTags, SubscribeTagChanges)
버전v1

엔드포인트 요약

메서드경로설명
GET/api/v1/tags/{tag_name}단일 태그 조회
POST/api/v1/tags:batchRead복수 태그 조회 (최대 500개)
POST/api/v1/tags/{driver_id}/{address}/write단일 태그 쓰기
POST/api/v1/tags:batchWrite복수 태그 쓰기 (최대 500개)
GET/api/v1/tags태그 목록 조회 (페이지네이션)
gRPC StreamSubscribeTagChanges태그 변경 실시간 구독

상세 엔드포인트

ReadTag

단일 태그의 현재 값을 조회합니다.

gRPC 메서드

rpc ReadTag (ReadTagRequest) returns (ReadTagResponse)

요청

파라미터타입필수설명
tag_namestringY태그 이름 (예: "PLC01.D100")

응답

{
"tag": {
"name": "PLC01.D100",
"driver": "Sim-PLC-01",
"dataType": "Float",
"accessMode": "ReadWrite",
"lastUpdated": "2026-02-17T10:30:00Z",
"currentValue": {
"doubleValue": 123.45
},
"quality": "Good"
}
}

응답 코드

코드설명
OK성공
NOT_FOUND태그를 찾을 수 없음
UNAUTHENTICATED인증 필요

REST 경로

GET /api/v1/tags/{tag_name}

ReadTags

여러 태그를 한 번에 조회합니다. (배치 읽기)

gRPC 메서드

rpc ReadTags (ReadTagsRequest) returns (ReadTagsResponse)

요청

파라미터타입필수설명
tag_namesstring[]Y태그 이름 배열 (최대 500개)

응답

{
"tags": [
{
"name": "PLC01.D100",
"driver": "Sim-PLC-01",
"dataType": "Float",
"accessMode": "ReadWrite",
"lastUpdated": "2026-02-17T10:30:00Z",
"currentValue": {
"doubleValue": 123.45
},
"quality": "Good"
}
],
"errors": [
{
"code": "TAG_NOT_FOUND",
"message": "Tag 'PLC01.D999' not found",
"metadata": {
"tagName": "PLC01.D999"
}
}
]
}

특징

  • 부분 실패 허용: 일부 태그 조회 실패 시 errors 배열에 포함
  • 성공한 태그는 tags 배열에 반환

REST 경로

POST /api/v1/tags:batchRead
Content-Type: application/json

{
"tagNames": ["PLC01.D100", "PLC01.D200", "PLC01.D300"]
}

WriteTag

단일 태그에 값을 씁니다.

gRPC 메서드

rpc WriteTag (WriteRequest) returns (WriteResponse)

요청

파라미터타입필수설명
driver_idstringY드라이버 ID (예: "Sim-PLC-01")
addressstringY태그 주소 (예: "D100")
valuestringYJSON 친화적 문자열 값

응답

{
"success": true,
"message": "Write Command Queued"
}

응답 코드

코드설명
OK요청 성공 (큐 등록)
INVALID_ARGUMENT잘못된 파라미터
NOT_FOUND드라이버 또는 태그를 찾을 수 없음
PERMISSION_DENIED읽기 전용 태그 쓰기 시도

REST 경로

POST /api/v1/tags/{driver_id}/{address}/write
Content-Type: application/json

{
"value": "123.45"
}

WriteTags

여러 태그에 값을 씁니다. (배치 쓰기)

gRPC 메서드

rpc WriteTags (WriteTagsRequest) returns (WriteTagsResponse)

요청

파라미터타입필수설명
writesTagWriteItem[]Y쓰기 항목 배열 (최대 500개)

TagWriteItem 구조

필드타입설명
tag_namestring태그 이름
valueTagValue태그 값 (다형성 oneof)

TagValue 구조 (oneof)

{
"stringValue": "text", // string
"doubleValue": 123.45, // double
"int64Value": 1000, // int64
"boolValue": true, // bool
"bytesValue": "base64..." // bytes
}

요청 예시

{
"writes": [
{
"tagName": "PLC01.D100",
"value": {
"doubleValue": 100.5
}
},
{
"tagName": "PLC01.D200",
"value": {
"int64Value": 500
}
}
]
}

응답

{
"successCount": 1,
"errors": [
{
"code": "READ_ONLY_TAG",
"message": "Tag 'PLC01.D200' is read-only",
"metadata": {
"tagName": "PLC01.D200"
}
}
]
}

REST 경로

POST /api/v1/tags:batchWrite
Content-Type: application/json

GetTagList

태그 목록을 페이지네이션으로 조회합니다.

gRPC 메서드

rpc GetTagList (GetTagListRequest) returns (GetTagListResponse)

요청

파라미터타입필수설명
driver_filterstringN드라이버 이름 필터
name_patternstringNLIKE 검색 패턴 (예: "%D1%")
paginationPaginationRequestN페이지 정보

PaginationRequest 구조

필드타입기본값설명
pageint321페이지 번호 (1-based)
page_sizeint3250페이지 크기 (최대 500)

응답

{
"tags": [
{
"name": "PLC01.D100",
"driver": "Sim-PLC-01",
"dataType": "Float",
"accessMode": "ReadWrite",
"lastUpdated": "2026-02-17T10:30:00Z",
"currentValue": {
"doubleValue": 123.45
},
"quality": "Good"
}
],
"totalCount": 1523
}

REST 경로

GET /api/v1/tags?driverFilter=Sim-PLC-01&namePattern=%D1%&page=1&pageSize=100

SubscribeTagChanges

태그 값 변경을 실시간으로 스트리밍합니다. (gRPC Server Streaming)

gRPC 메서드

rpc SubscribeTagChanges (SubscribeTagChangesRequest) returns (stream TagChangeNotification)

요청

파라미터타입필수설명
tag_namesstring[]Y구독할 태그 목록
throttle_msint32N최소 전송 간격 (기본 100ms)

스트림 응답

{
"tagName": "PLC01.D100",
"newValue": {
"doubleValue": 150.75
},
"timestamp": "2026-02-17T10:31:00Z",
"quality": "Good"
}

특징

  • 서버 → 클라이언트 스트리밍
  • throttle_ms로 과도한 업데이트 제한 가능
  • 연결 유지 중 지속적으로 변경 사항 전송

데이터 타입

TagValue (다형성)

태그 값은 다음 타입 중 하나를 가질 수 있습니다:

타입필드명설명
stringstring_value문자열
doubledouble_value부동소수점 (64bit)
int64int64_value정수 (64bit)
boolbool_value불리언
bytesbytes_value바이너리 데이터

Quality 값

설명
Good정상 값
Bad통신 실패 또는 에러
Uncertain값의 신뢰도가 낮음

AccessMode 값

설명
ReadOnly읽기 전용
ReadWrite읽기/쓰기 가능

사용 예제

C# (CaffeineClient)

using Caffeine.Client;

var client = new CaffeineClient();
await client.ConnectAsync("https://localhost:5001");

// 단일 태그 읽기
var readResponse = await client.ReadTagAsync("PLC01.D100");
Console.WriteLine($"Current Value: {readResponse.Tag.CurrentValue.DoubleValue}");

// 복수 태그 읽기
var batchResponse = await client.ReadTagsAsync(new[] { "PLC01.D100", "PLC01.D200" });
foreach (var tag in batchResponse.Tags)
{
Console.WriteLine($"{tag.Name} = {tag.CurrentValue}");
}

// 단일 태그 쓰기
bool success = await client.WriteTagAsync("Sim-PLC-01", "D100", 999.0);

// 복수 태그 쓰기
var writeResponse = await client.WriteTagsAsync(new[]
{
new TagWriteItem { TagName = "PLC01.D100", Value = new TagValue { DoubleValue = 100.5 } },
new TagWriteItem { TagName = "PLC01.D200", Value = new TagValue { Int64Value = 500 } }
});
Console.WriteLine($"Success: {writeResponse.SuccessCount}");

// 태그 목록 조회
var listResponse = await client.GetTagListAsync(driverFilter: "Sim-PLC-01", page: 1, pageSize: 50);
Console.WriteLine($"Total Tags: {listResponse.TotalCount}");

await client.DisposeAsync();

C# (직접 gRPC 호출)

using Grpc.Net.Client;
using Caffeine.IPC.Grpc;

var channel = GrpcChannel.ForAddress("https://localhost:5001");
var grpcClient = new CaffeineApi.CaffeineApiClient(channel);

// 단일 태그 읽기
var readResponse = await grpcClient.ReadTagAsync(new ReadTagRequest
{
TagName = "PLC01.D100"
});

Console.WriteLine($"Value: {readResponse.Tag.CurrentValue.DoubleValue}");

// 배치 읽기
var batchReadResponse = await grpcClient.ReadTagsAsync(new ReadTagsRequest
{
TagNames = { "PLC01.D100", "PLC01.D200", "PLC01.D300" }
});

foreach (var tag in batchReadResponse.Tags)
{
Console.WriteLine($"{tag.Name} = {tag.CurrentValue}");
}

// 배치 쓰기
var writeResponse = await grpcClient.WriteTagsAsync(new WriteTagsRequest
{
Writes =
{
new TagWriteItem
{
TagName = "PLC01.D100",
Value = new TagValue { DoubleValue = 100.5 }
}
}
});

Console.WriteLine($"Success Count: {writeResponse.SuccessCount}");

// 실시간 구독
using var subscription = grpcClient.SubscribeTagChanges(new SubscribeTagChangesRequest
{
TagNames = { "PLC01.D100", "PLC01.D200" },
ThrottleMs = 200
});

await foreach (var notification in subscription.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"{notification.TagName} changed to {notification.NewValue} at {notification.Timestamp}");
}

curl (REST API)

단일 태그 읽기

curl -X GET "https://localhost:5001/api/v1/tags/PLC01.D100" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"

응답

{
"tag": {
"name": "PLC01.D100",
"driver": "Sim-PLC-01",
"dataType": "Float",
"accessMode": "ReadWrite",
"lastUpdated": "2026-02-17T10:30:00.000Z",
"currentValue": {
"doubleValue": 123.45
},
"quality": "Good"
}
}

복수 태그 읽기

curl -X POST "https://localhost:5001/api/v1/tags:batchRead" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-d '{
"tagNames": ["PLC01.D100", "PLC01.D200"]
}'

단일 태그 쓰기

curl -X POST "https://localhost:5001/api/v1/tags/Sim-PLC-01/D100/write" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-d '{
"value": "999.0"
}'

복수 태그 쓰기

curl -X POST "https://localhost:5001/api/v1/tags:batchWrite" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-d '{
"writes": [
{
"tagName": "PLC01.D100",
"value": {
"doubleValue": 100.5
}
},
{
"tagName": "PLC01.D200",
"value": {
"int64Value": 500
}
}
]
}'

태그 목록 조회

curl -X GET "https://localhost:5001/api/v1/tags?driverFilter=Sim-PLC-01&page=1&pageSize=50" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"

에러 코드

코드설명HTTP 상태
TAG_NOT_FOUND태그를 찾을 수 없음404
INVALID_DRIVER드라이버 ID가 잘못됨400
READ_ONLY_TAG읽기 전용 태그 쓰기 시도403
INVALID_VALUE잘못된 값 타입400
DRIVER_OFFLINE드라이버가 오프라인 상태503
BATCH_SIZE_EXCEEDED배치 크기 초과 (최대 500개)400

성능 최적화

배치 읽기/쓰기 사용

단일 호출 대신 ReadTags, WriteTags를 사용하여 네트워크 라운드트립을 줄입니다.

// ❌ 비효율적
foreach (var tagName in tagNames)
{
await client.ReadTagAsync(tagName);
}

// ✅ 효율적
var response = await client.ReadTagsAsync(tagNames);

Throttle 설정

SubscribeTagChanges에서 throttle_ms를 설정하여 과도한 업데이트를 방지합니다.

var subscription = grpcClient.SubscribeTagChanges(new SubscribeTagChangesRequest
{
TagNames = { "PLC01.D100" },
ThrottleMs = 500 // 최소 500ms 간격으로 전송
});

문제 해결

태그를 찾을 수 없음 (TAG_NOT_FOUND)

원인:

  • 태그 이름 오타
  • 드라이버가 해당 태그를 등록하지 않음
  • 드라이버 오프라인

해결:

  1. GetTagList로 등록된 태그 목록 확인
  2. 드라이버 상태 확인 (GetDriverStatus)
  3. 태그 이름 대소문자 확인

쓰기 실패 (PERMISSION_DENIED)

원인:

  • 읽기 전용 태그
  • JWT 토큰 권한 부족

해결:

  1. ReadTagaccessMode 확인
  2. JWT 토큰의 Role 확인 (Admin 권한 필요)

실시간 구독 데이터 수신 안 됨

원인:

  • 태그 값이 변경되지 않음
  • throttle_ms 설정이 너무 큼
  • 네트워크 연결 끊김

해결:

  1. 태그 값을 변경해보기 (WriteTag)
  2. throttle_ms 값 줄이기 또는 제거
  3. gRPC 연결 상태 확인

관련 문서