본문으로 건너뛰기

태그 관리 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. ReadTag로 accessMode 확인
  2. JWT 토큰의 Role 확인 (Admin 권한 필요)

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

원인:

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

해결:

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

관련 문서​