Formula (계산식, fomula) 사용 가이드
태그 등록 시 fomula 필드에 산술 수식을 넣으면, 드라이버가 PLC 에서 읽어온 원시 값(raw value)을 후처리하여 변환된 값으로 캐시 / Sparkplug / REST 응답에 사용합니다.
| 항목 | 값 |
|---|---|
| 구현 클래스 | plantpulse.app.edge.component.collector.plc.PLCValueFomula |
| 호출 위치 | PLCValueReader.read() 의 3 단계 (raw → format → fomula → 캐시 갱신) |
| 라이브러리 | plantpulse-edge-fomula.jar (plantpulse.edge.fomula.FormulaEngine) |
| 변수 치환 | ${VALUE} (현재 태그 값) / ${tag_id} (다른 태그의 마지막 값, LastValueMap 캐시) |
적용 대상 data_type | Float / Double / Integer / Long 만 적용. 그 외 타입(String, Boolean, …)은 그대로 통과 |
| 결과 | Formula.evaluate(...) 의 BigDecimal → 타입별 렌더(정수 계열은 정수, Float/Double 은 소수) |
동작 흐름
-
드라이버에서 raw 문자열 값을 받음 (예:
"16384"). -
PLCValueFomula.fomulaValue(address, value)호출. -
address.getFomula()가 비어있으면 그대로 반환. -
data_type이 Float / Double / Integer / Long 이 아니면 그대로 반환. -
계산식이 참조하는 이름을 확인 —
VALUE는 방금 읽은 값, 나머지는LastValueMap의 다른 태그 마지막 값. -
참조 태그가 아직 한 번도 수집되지 않았으면 예외:
Variable value referenced by the formula is not yet in cache : formula=[`<fomula>`], variable=[`<이름>`] -
계산식이 참조하는 이름만 값으로 바인딩해 평가 →
BigDecimal→ 타입별 문자열 반환.
변수 종류
| 표기 | 의미 | 출처 |
|---|---|---|
${VALUE} | 현재 태그의 raw 값 | 드라이버가 막 읽어온 값 |
${TAG_ID_X} | 다른 태그의 마지막 값 | LastValueMap (전체 태그 공용 캐시) |
LastValueMap 은 모든 태그의 마지막 raw / 후처리 값을 보관합니다. 한 OPC 의 다른 태그뿐 아니라 다른 OPC 의 태그도 참조 가능합니다 (단, 그 태그가 한 번이라도 수집되어 캐시에 존재해야 함).
${TAG_ZERO} 를 참조했는데 TAG_ZERO 가 아직 한 번도 수집되지 않았다면 값을 바인딩할 수 없어 예외가 발생합니다. 참조 태그의 timecycle 이 더 짧거나, 참조 태그를 먼저 수집되도록 auto_collect=true 로 등록 하세요. (저장 시점 문법 검사는 이 경우를 통과시킵니다 — 수집 순서 문제이지 수식 오류가 아닙니다.)
표현식 문법
엑셀과 같은 방식으로 씁니다. 함수 이름과 의미가 엑셀과 같으므로, 엑셀에서 쓰던 식을 거의
그대로 옮길 수 있습니다. 대소문자는 가리지 않습니다 (IF = if).
| 카테고리 | 토큰 | 비고 |
|---|---|---|
| 산술 | +, -, *, /, ^ | ^ 는 거듭제곱 |
| 비교 | >, <, >=, <=, =, <> | = 는 같음, <> 는 다름 (엑셀과 동일) |
| 그룹 | (, ) | 괄호 우선 |
| 상수 | PI, E | 원주율, 자연상수 |
| 조건 | IF, SWITCH, AND, OR, NOT | IF(조건, 참일때, 거짓일때) |
| 반올림 | ROUND, ROUNDUP, ROUNDDOWN, CEILING, FLOOR, INT, TRUNC | ROUND(값, 자리수) |
| 수치 | ABS, SIGN, MOD, POWER, SQRT, CBRT, EXP, FACT | MOD 의 나머지 부호는 제수를 따름 |
| 집계 | MIN, MAX, SUM, AVERAGE, COALESCE | 인자 여러 개 |
| 로그 | LOG, LOG10, LN | LOG = 상용로그(밑10), LN = 자연로그 |
| 삼각 | SIN, COS, TAN, ASIN, ACOS, ATAN, ATAN2 | 라디안 기준 (엑셀과 동일) |
| 쌍곡 | SINH, COSH, TANH, ASINH, ACOSH, ATANH | |
| 각도 변환 | DEGREES, RADIANS | 라디안 ↔ 도 |
- 비트 연산 (
&,|) - 사용자 정의 함수
- 문자열 연산 / 날짜 연산 이런 게 필요하면 후속 단계 (외부 함수 / 후처리 노드) 에서 다루세요.
문법이 맞지 않는 계산식은 저장 시점에 거부됩니다 (UI 저장 / CSV 업로드 / 백업 복원 모두). 태그 설정 모달에서 샘플 값을 넣어 결과를 미리 확인할 수 있습니다.
계산 중 실패(0 으로 나누기, 참조 값이 숫자가 아님 등)는 수집 오류로 기록되고 해당 태그의 품질이 떨어집니다. 그럴듯한 숫자를 대신 저장하지 않습니다.
변수 표기
${VALUE} 와 맨이름 VALUE 둘 다 씁니다. 다른 태그도 마찬가지입니다 —
${TAG_ZERO} = TAG_ZERO. 기존에 저장된 ${...} 계산식은 그대로 동작합니다.
${VALUE}*0.1 기존 표기
VALUE*0.1 같은 뜻
IF(VALUE>100, 100, VALUE*0.1) 엑셀식
풍부한 예제 모음
1. 단순 스케일 (×0.1)
| 항목 | 값 |
|---|---|
data_type | Float |
fomula | ${VALUE}*0.1 |
| 입력 → 출력 | 16384 → 1638.4 |
용도: 정수 raw → 소수점 1자리 실수. 온도 / 압력 / 유량 센서가 정수로 값을 보낼 때.
2. 단위 변환 (mV → V)
| 항목 | 값 |
|---|---|
data_type | Float |
fomula | ${VALUE}/1000 |
| 입력 → 출력 | 3300 → 3.3 |
3. 오프셋 보정 (섭씨 → 켈빈)
| 항목 | 값 |
|---|---|
data_type | Float |
fomula | ${VALUE}+273.15 |
| 입력 → 출력 | 25 → 298.15 |
4. 다른 태그 참조 (영점 보정)
| 항목 | 값 |
|---|---|
data_type | Float |
fomula | ${VALUE}-${TAG_ZERO} |
| 입력 → 출력 | VALUE=1024, TAG_ZERO=24 → 1000 |
TAG_ZERO 는 다른 태그의 ID. 영점 / Tare 보정에 사용.
5. 다항식 (제곱)
| 항목 | 값 |
|---|---|
data_type | Float |
fomula | ${VALUE}*${VALUE}*0.001 |
| 입력 → 출력 | 100 → 10.0 |
6. 함수 — 제곱근
| 항목 | 값 |
|---|---|
data_type | Float |
fomula | sqrt(${VALUE}) |
| 입력 → 출력 | 144 → 12.0 |
7. 함수 — 삼각 (sin, radian)
| 항목 | 값 |
|---|---|
data_type | Float |
fomula | sin(${VALUE}) |
| 입력 → 출력 | 1.5708 (≈π/2) → 1.0 |
각도(degree) 입력이라면 sin(${VALUE}*pi/180).
8. 다중 태그 — 캘리브레이션 (gain × x + offset)
| 항목 | 값 |
|---|---|
data_type | Float |
fomula | ${VALUE}*${TAG_GAIN}+${TAG_OFFSET} |
| 입력 → 출력 | VALUE=100, GAIN=0.05, OFFSET=2 → 7.0 |
장비별 보정 계수를 별도 태그(또는 HTTP-bind 태그)로 관리하는 패턴.
9. 형변환 / 강제 실수화
| 항목 | 값 |
|---|---|
data_type | Float |
fomula | ${VALUE}*1.0 |
| 입력 → 출력 | 123 → 123.0 |
Integer 로 들어온 값을 Float 로 캐스팅만 하고 싶을 때.
10. 거듭제곱 / 지수
| 항목 | 값 |
|---|---|
data_type | Float |
fomula | ${VALUE}^2 |
| 입력 → 출력 | 5 → 25.0 |
11. 로그
| 항목 | 값 |
|---|---|
data_type | Float |
fomula | log(${VALUE}) |
| 입력 → 출력 | 100 → 2.0 |
ln(...) 도 사용 가능 (자연로그).
12. 복합 — RMS 의 dB 변환
| 항목 | 값 |
|---|---|
data_type | Float |
fomula | 20*log(${VALUE}) |
| 입력 → 출력 | 1000 → 60.0 |
REST 등록 예 (curl)
태그 단건 등록:
curl -X POST http://<edge-host>/api/v1/opc/OPC_LS_XBM_0001/tag \
-H "Content-Type: application/json" \
-d '{
"tag_id": "OPC_LS_XBM_0001_TAG_PRESS",
"tag_name": "Pressure (kPa)",
"plc_address": "D00100",
"data_type": "Float",
"format": "REAL",
"fomula": "${VALUE}*0.1",
"description": "스케일 ×0.1 적용"
}'
OPC + 태그 묶음 등록 시 tag_list[] 안에 fomula 를 넣어주면 됩니다.
값 read (후처리된 값이 반환됨):
curl -s http://<edge-host>/api/v1/tag/OPC_LS_XBM_0001_TAG_PRESS/value | jq
# {
# "result": "OK",
# "data": {
# "tag_id": "OPC_LS_XBM_0001_TAG_PRESS",
# "value": "1638.4",
# ...
# }
# }
흔한 에러 + 해결
| 메시지 / 증상 | 원인 | 해결 |
|---|---|---|
계산식에 해당하는 변수값이 아직 캐시에 없습니다 : 계산식=[${VALUE}-${TAG_ZERO}] | 참조 태그 (TAG_ZERO) 가 아직 한 번도 수집된 적 없음 | 참조 태그를 먼저 등록 + auto_collect=true 로 한 사이클 돌려두기 |
계산식이 참조하는 값이 숫자가 아닙니다 | 참조 태그의 값이 숫자가 아님 (예: data_type=String 인 태그를 참조) | 숫자 타입 태그를 참조하도록 수정 |
Closing brace not found / Missing second operand | 괄호 짝이 안 맞거나 연산자 뒤 피연산자 누락 | 저장 시점에 거부되므로 모달의 오류 메시지가 위치까지 알려줍니다 |
| 후처리가 안 적용됨 (raw 그대로 반환) | data_type 이 String/Boolean | Float/Double/Integer/Long 으로 변경하거나 후처리 필요시 별도 가공 단계 사용 |
결과가 항상 0 | raw 값이 실제로 0 | raw read 값을 먼저 GET /api/v1/tag/.../value 로 확인. (옛 파서는 ${VALUE}* 같은 불완전 수식을 조용히 0 으로 계산했지만, 지금은 그런 수식이 저장 단계에서 거부됩니다.) |
수집 오류 FORMAT_FAILED + 품질 저하 | 0 으로 나누거나 sqrt 음수 등 계산 실패 | Infinity/NaN 을 저장하지 않고 오류로 드러냅니다. 수식에 가드를 넣으세요 — 예: IF(TAG_ZERO=0, 0, VALUE/TAG_ZERO) |
동작 메모
LastValueMap은 싱글톤 (in-memory). 게이트웨이 재시작 시 초기화 → 첫 사이클은 다른 태그 참조가 실패할 수 있음.- 계산은 전 구간
BigDecimal이라Long/QWord의 큰 정수도 마지막 자리까지 보존됩니다(옛double평가에서 나던 손실 없음). - 결과는
Double.toString()→ 문자열 캐시. 화면 / Sparkplug 발행 시 다시data_type으로 캐스팅됩니다 (SparkplugDataTypeMapper참고). - 같은 OPC 안에서 태그 여러 개의 fomula 가 서로를 참조하면, 수집 순서 에 따라 첫 사이클이 부분적으로 실패할 수 있습니다. 운영상 큰 문제는 아니지만(다음 사이클부터 정상), 깨끗하게 가려면 참조 대상 태그를 더 짧은
timecycle의 별도 OPC 에 두세요.