태그 관리 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 Stream | SubscribeTagChanges | 태그 변경 실시간 구독 |
상세 엔드포인트
ReadTag
단일 태그의 현재 값을 조회합니다.
gRPC 메서드
rpc ReadTag (ReadTagRequest) returns (ReadTagResponse)
요청
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| tag_name | string | Y | 태그 이름 (예: "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_names | string[] | 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_id | string | Y | 드라이버 ID (예: "Sim-PLC-01") |
| address | string | Y | 태그 주소 (예: "D100") |
| value | string | Y | JSON 친화적 문자열 값 |
응답
{
"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)
요청
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| writes | TagWriteItem[] | Y | 쓰기 항목 배열 (최대 500개) |
TagWriteItem 구조
| 필드 | 타입 | 설명 |
|---|---|---|
| tag_name | string | 태그 이름 |
| value | TagValue | 태그 값 (다형성 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_filter | string | N | 드라이버 이름 필터 |
| name_pattern | string | N | LIKE 검색 패턴 (예: "%D1%") |
| pagination | PaginationRequest | N | 페이지 정보 |
PaginationRequest 구조
| 필드 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| page | int32 | 1 | 페이지 번호 (1-based) |
| page_size | int32 | 50 | 페이지 크기 (최대 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_names | string[] | Y | 구독할 태그 목록 |
| throttle_ms | int32 | N | 최소 전송 간격 (기본 100ms) |
스트림 응답
{
"tagName": "PLC01.D100",
"newValue": {
"doubleValue": 150.75
},
"timestamp": "2026-02-17T10:31:00Z",
"quality": "Good"
}
특징
- 서버 → 클라이언트 스트리밍
throttle_ms로 과도한 업데이트 제한 가능- 연결 유지 중 지속적으로 변경 사항 전송
데이터 타입
TagValue (다형성)
태그 값은 다음 타입 중 하나를 가질 수 있습니다:
| 타입 | 필드명 | 설명 |
|---|---|---|
| string | string_value | 문자열 |
| double | double_value | 부동소수점 (64bit) |
| int64 | int64_value | 정수 (64bit) |
| bool | bool_value | 불리언 |
| bytes | bytes_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)
원인:
- 태그 이름 오타
- 드라이버가 해당 태그를 등록하지 않음
- 드라이버 오프라인
해결:
GetTagList로 등록된 태그 목록 확인- 드라이버 상태 확인 (
GetDriverStatus) - 태그 이름 대소문자 확인
쓰기 실패 (PERMISSION_DENIED)
원인:
- 읽기 전용 태그
- JWT 토큰 권한 부족
해결:
ReadTag로accessMode확인- JWT 토큰의 Role 확인 (
Admin권한 필요)
실시간 구독 데이터 수신 안 됨
원인:
- 태그 값이 변경되지 않음
throttle_ms설정이 너무 큼- 네트워크 연결 끊김
해결:
- 태그 값을 변경해보기 (
WriteTag) throttle_ms값 줄이기 또는 제거- gRPC 연결 상태 확인