> For the complete documentation index, see [llms.txt](https://metatron-grandview.gitbook.io/meatatron-grandview/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://metatron-grandview.gitbook.io/meatatron-grandview/iotdevicedev/policy-profile-plc-i-o.md).

# Policy profile 에 PLC I/O 설정

## PLC I/O 설정과 policy profile

### PLC I/O 설정의 장점&#x20;

* Policy profile 을 통해 PLC I/O(Input/Output)를 처리하면, 펌웨어는 configuration 을 갖고 있지 않아도 되므로 IoT 디바이스 개발사 입장에서 IoT 디바이스와 펌웨어를 관리하기 용이해집니다. <br>
* IoT 디바이스가 부팅 될 때와 실행 중일 때 자동으로 업데이트하는 기능을 구현하면, 현장 출동을 하지 않아도 PLC I/O 설정을 원격에서 변경할 수 있습니다. <br>
* 원격 관리로 여러 현장을 돌아보는 시간을 절약할 수 있습니다.

Policy profile 의 구조에서 다음 위치에 PLC I/O 설정을 추가할 수 있습니다.

```
"desired": {
    "policy": {
        "ver": {
            "maj": 1,
            "mnr": 0
        },
        "fw": 1.0,
        "brate": 9600,
        "sec": 120,
        "devices": {
          "sensor1": {
            "idx": 0,
            // PLC I/O 상세 설정
          },
          "sensor2": {
            "idx": 1
            // PLC I/O 상세 설정
          }
        }
    }
}
```

## PLC I/O 설정의 구문 형식

다음과 같은 구문 형식을 가집니다.

```
"devices": {
  "Device ID": {
    "idx": 인덱스 번호 / MODBUS slave ID,
    "opts": {
    },
    "데이터 이름 1": "PLC I/O 설정값",
    "데이터 이름 2": "PLC I/O 설정값",
    ...
  }
}
```

* **idx 필드는 Slave ID**\
  여기서 idx 필드는 MODBUS 네트워크의 slave ID 로 활용합니다.

```
"devices": {
  "plc1": {
    "idx": 인덱스 번호,
    "opts":{
    },
    "오일 온도": "PLC I/O 설정값",
    ...
  }
}
```

* **데이터 이름 필드**\
  예를 들어 PLC 에서 오일 온도 값을 읽는다고 가정합니다. 오일 온도 값은 PLC 의 1 번지 address 에서 holding register 값을 읽을 수 있습니다. 그런 경우 "오일 온도" 라는 값을 이름 필드로 사용하면 됩니다.
* **opts 필드**\
  IoT 디바이스의 부가 설정정보를 자유롭게 지정하고 싶을 때 사용할 수 있는 필드입니다.

오일 온도라는 변수 값은 서버로 전송할 메시지 포맷에서 contents 필드 아래의 변수로 지정하면 됩니다.

```
{
  "deviceId": "plc1",
  "contents": {
    "오일 온도": 1234,
  }
}
```

devices 필드 바로 아래의 plc1 이란 tag 값이 서버로 전송할 데이터의 deviceId 로 사용되는 것 또한 주의해야합니다.&#x20;

### PLC I/O 상세 설정 방법

PLC I/O 설정은 다음과 같은 구문 형식을 갖습니다.

```
"데이터 이름": {
  "op": PLC 로 데이터를 읽는 MODBUS function code 의 index 값,
  "addr": 데이터를 읽는 주소,
  "tp": Register 공간에서 저장되는 값의 변수 type 의 index 값,
  "edn": Register 공간에서 사용하고 있는 endian 모드의 index 값
  "sc": (옵션) Register 값에 곱셈을 통해 값 변환해야 하는 경우 설정,
  "sp": (옵션) Register 공간에서 바이트를 나누어 실수 데이터의 정수, 소수를 저장하는 경우 변환을 위해 설정
}
```

* **op 필드(Operation code 필드)**\
  op 필드는 MODBUS function 의 종류를 지정합니다.

| **Function name**      | **Function code** | <p><strong>op 필드에서</strong> <br><strong>사용할 index 값</strong></p> | <p><strong>부동소수점 표기 대응</strong></p><p>(type(tp), endian(edn), sc(scale), <br>sp(split) 를 추가 설정 필요)</p> |
| ---------------------- | :---------------: | :--------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------: |
| Read Discrete Inputs   |         2         |                               **0**                              |                                                    X                                                   |
| Read Coils             |         1         |                               **1**                              |                                                    X                                                   |
| Read Input Registers   |         4         |                               **2**                              |                                                    O                                                   |
| Read Holding Registers |         3         |                               **3**                              |                                                    O                                                   |

실제 MODBUS function 코드는 위 표의 index 값과 대응하도록 펌웨어를 개발해야 합니다. \
서버에서 내려주는 값을 그대로 쓰지 않고 매핑을 다시하는 이유는 실수로 설비에 잘못된 I/O 가 발생하는 것을 방지하기 위함입니다.

op 필드 index 값이 0, 1 인 read discrete inputs 와 read coils 는 1 bit 만 읽을 수 있기 때문에 소수점 표기 \
대응을 할 필요가 없지만 나머지는 더 긴 숫자를 저장할 수 있어 PLC 설정에 따라 소수점을 정상적으로 변환하기 위해서 tp, edn, sc 등과 같은 추가 설정이 있습니다.

* **addr 필드(PLC Address 필드)**\
  addr 필드에는 I/O 를 수행할 PLC 주소를 지정합니다.<br>
* **tp 필드 (Variable Type 필드)**\
  tp 필드는 PLC 에 저장된 데이터가 어떤 변수 형식인지 지정합니다.

| **타입**         | **길이**   | **값의 종류**       | **tp 필드에 사용할 index 값** |
| -------------- | -------- | --------------- | ---------------------- |
| boolean        | 1 바이트    | true, false     | **0**                  |
| int16          | 2 바이트    | 정수              | **1**                  |
| unsigned int16 | 2 바이트    | 0 이상의 정수        | **2**                  |
| int32          | 4 바이트    | 더 긴 정수 저장       | **3**                  |
| unsigned int32 | 4 바이트    | 0 이상의 더 긴 정수 저장 | **4**                  |
| float          | 4 바이트    | 실수 저장 (소수점 지원)  | **5**                  |
| bit            | 1 bit    | 비트              | **6**                  |
| bit range      | 0\~15bit | 범위 지정 비트        | **7**                  |

tp 값 5에 해당하는 경우 PLC 저장 공간에 실수형(float type) 데이터가 저장되어 있기 때문에 endian 변환만 수행하고 바로 서버로 전송할 수 있습니다. 그러나 정수를 저장하면서 실제로는 소수점을 저장하고 있는 \
경우에는 sc, sp 추가 옵션을 통해 소수점 변환을 수행하고 서버로 데이터를 보내야 합니다.

{% hint style="info" %}
PLC 개발 방법이 표준적이지 않으며 개인 개발자의 구성 방식에 의존적이기 때문에 데이터를 PLC 에 저장하는 방법에는 여러가지 유형이 있을 수 있습니다.
{% endhint %}

예를 들어 12.34 라는 값을 정수 부분과 소수 부분을 나누어 1234 로 PLC 주소에 저장하고 있을 수 있습니다. 그런 경우 tp 는 1\~4 같은 정수 설정으로 지정합니다. 그리고 sc 또는 sp 설정에서 12.34 로 변환하게 합니다.

tp 가 6인 것은, 실제로 비트 값을 저장하면서 register 에 저장하고 있는 경우 읽는 방법입니다. \
tp 가 6으로 지정되면 index 라는 추가 필드를 두고, 몇 번째 bit 인덱스에서 값을 읽을지 정합니다. \
펌웨어는 우선 register 에서 2 또는 4바이트를 읽지만 실제로는 index 에 지정된 비트 값을 추출해서 서버로 보내야 합니다.

```
{
  "machineOn": {
    ...,
    "addr": 123,
    "tp": 6,
    "index": 3,
    ...
  },
  "cleaning": {
    ...,
    "addr": 123,
    "tp": 6,
    "index": 4
  }
}
```

위 예시에서는 PLC 의 123 번지에서 읽은 다음 3번째 비트만 추출해서 machineOn 이라는 변수에 지정하라는 설정입니다. 그리고 이어서 같은 번지에서 읽지만 4번째 비트만 추출해서 cleaning 변수로 저장하는 예시가 있습니다.

tp 7 은 여러 bit 를 읽으라는 설정입니다. 예를 들어 2바이트 PLC 주소 공간에 상위 1바이트, 하위 1바이트 나누어 데이터를 저장하는 경우 이 설정을 통해 데이터를 추출할 수 있습니다. tp 가 7인 경우 start, length, unsigned 필드를 추가로 갖습니다.

```
{
  "boxTape": {
    ...,
    "addr": 124,
    "tp": 7,
    "start": 8,
    "length": 8,
    "unsigned": false
  }
}
```

이 설정은 124번지를 읽어서 9번째 비트부터 8개의 비트를 읽어서 boxTape 변수에 저장하고 서버로 보내라는 설정입니다. 이 값은 unsigned 가 false 이므로, 음수를 가질 수 있는 변수입니다.

* **edn 필드(Endian Mode 필드)**\
  edn 필드는 PLC 에 저장된 값이 어떤 endian mode 로 저장되었는지 설정합니다. 대부분의 서버와 개인 컴퓨터들은 big endian 을 사용하지만, 일부 PLC 들은 드물게 small endian 등 다른 모드를 사용하는 \
  경우가 있기 때문에 펌웨어는 이 필드 설정에 따라 값을 변환해서 서버로 전송해야 합니다.

| **길이** | **구분**            | **표기 방식** | **edn 필드에 사용할 index 값** | **Big endian 변환 필요** |
| ------ | ----------------- | --------- | ----------------------- | -------------------- |
| 2 바이트  | Big endian        | AB        | **0**                   | X                    |
|        | Small endian      | BA        | **1**                   | O                    |
| 4 바이트  | Big endian        | ABCD      | **2**                   | X                    |
|        | Small endian      | DCBA      | **3**                   | O                    |
|        | Mid-Big endian    | BADC      | **4**                   | O                    |
|        | Mid-Little endian | CDAB      | **5**                   | O                    |

Big endian 모드 변환이 필요하다고 된 변수들을 위해서 펌웨어는 endian 변환 로직을 갖고 있어야 합니다.

####

* **sc 와 sp 필드 (Scale, Split)**\
  sc 와 sp 는 소수점을 float 형식으로 저장하지 않고 integer 형식으로 저장하고 있는 PLC address 값을 변환하기 위해 사용합니다.

| **mode** | **config** | **설명**                         | **펌웨어 개발 필요한 것**                                                                                                                                                                                                                    |
| -------- | ---------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| "sp"     | 숫자         | 왼쪽에서 몇 바이트까지 정수 부분인지 설정한다.     | <p>설정된 바이트까지 정수로 하고, 나머지는 소수부로 <br>하여 float 으로 변환한 후 서버로 전송<br></p><p>예를 들어 2바이트 값의 첫 1 바이트가 정수 부분, <br>다음 1바이트가 소수 부분이라면 sp 가 1로 지정된다.</p><p><strong>원본 데이터가 2바이트 type 이면 split 은 1만 허용되고, 4바이트이면 split 이 1과 2만 허용된다.</strong></p> |
| "sc"     | 숫자         | 데이터를 변환하기 위해 얼마를 곱하면 되는지 결정한다. | <p>36.5 를 365로 표기하는 PLC 필드는, <strong>"sc": 0.1</strong> 으로 <br>설정한다.</p><p>또는 특정한 값을 곱하여 더 값을 길게 만들어야 한다면 <strong>"sc": 100</strong> 과 같이 지정하여 값을 길게 만들 수 있다.</p>                                                                   |

12.34 를 1234로 저장한 경우 아래 설정으로 데이터 변환 방법을 정의합니다.

```
{
   "scaleVar": {
     "tp": 2,
     "sc": 0.01
   }
 }
```

sc 에 지정된 값을 곱하여 값을 변환하면 됩니다.\
sp 는 바이트 단위로 split 합니다. 그리고 sc 와 sp 는 동시에 설정될 수 없습니다.

## 데이터 수집 제약

디바이스 트윈은 32KB 의 용량 제약이 있습니다. 그렇기 때문에 policy profile 로 PLC I/O 를 설정하게 되면, 디바이스 트윈의 용량 제약 때문에 slave 개수와 설정 가능한 address 개수에 제약이 발생합니다.

* **Policy 필드**\
  단순 JSON text 길이로만 76바이트가 기본적으로 소모

| **바이트 수** | JSON                                                                         |
| --------- | ---------------------------------------------------------------------------- |
| 76        | "policy":{"ver":{"maj":99,"mnr":99},"brate":115200,"sec":86400,"devices":{}} |

* **Device ID 필드**

| **바이트 수** | JSON                      |
| --------- | ------------------------- |
| 25        | "0123456789":{"idx":30,}, |

* **데이터 필드 (Address map 관련 설정 부분)**

| **바이트 수** | JSON                                                                   |
| --------- | ---------------------------------------------------------------------- |
| 70        | "012345678901234":{"op":3,"addr":9999999,"tp":5,"edn":5,"sc":0.00001}, |

* **전체 사이즈 예상 계산 공식**\
  전체 사이즈 = 76 + 수집 장치 개수(Device ID 개수) \* 25 + PLC Address 개수 \* 70 \* Device ID 개수\
  \
  1대의 게이트웨이에 PLC 가 10대, 각 PLC 로부터 읽는 데이터 필드 수(Address 개수)가 60개라고 하면 \
  최대 메모리 필요량은 다음과 같이 계산될 수 있음.\
  \
  **76 + 10 \* 25 + 60 \* 70 \* 10 = 42326 (byte) = 41.3 (KB)**\
  32KB 를 넘어가기 때문에 게이트웨이를 추가하여 PLC 를 나뉘어 연결 필요&#x20;

* **아키텍처 구상에 필요한 정보**\
  게이트웨이 1대가 최대 수집할 수 있는 PLC 데이터 필드 개수

| **PLC** | **Address 수** | **사이즈** |
| ------- | ------------- | ------- |
| 1       | 450           | 31KB    |
| 1       | 145           | 10KB    |

게이트웨이 1대가 수집할 수 있는 데이터 최대는 PLC 1대에만 연결될 경우 450개 데이터를 얻을 수 있다.

* **데이터를 모두 읽는데 필요한 시간 예측**

| **Address 수** | **사이즈** | **Timeout** | **필요 시간**    | **최소 데이터 전송 주기** |
| ------------- | ------- | ----------- | ------------ | ---------------- |
| 450           | 31KB    | 0.5초        | 225초 (약 4분)  | 5분 이상            |
| 145           | 10KB    | 0.5초        | 72.5초 (약 2분) | 3분 이상            |

* **최대 PLC 가 연결될 경우 수집할 수 있는 Address 수**

| **PLC** | **Address 수** | **사이즈** |
| ------- | ------------- | ------- |
| 30      | 15            | 31.5KB  |
| 30      | 4             | 9KB     |

{% hint style="warning" %}
결론적으로 1-30 대까지의 slave 수, 1-450 개의 수집 address 수, 그리고 32KB 제약을 따라 수집 체계가 결정될 수 있습니다.
{% endhint %}

## Example Policy profile

예제 policy profile 을 통해 실제 어떻게 I/O 가 구성되고 서버에 보낼 데이터가 결정되는지 알아보겠습니다.

```
{
  "desired":{
    "policy":{
      "ver":{
        "maj":1,
        "mnr":0
      },
      "brate":115200,
      "sec":300,
      "devices":{
        "press-1":{
          "idx":1,
          "intTemp":{
            "op":1,
            "addr":1
          },
          "mntLamp":{
            "op":0,
            "addr":2
          }
        },
        "cutter-1":{
          "idx":2,
          "oilGauge":{
            "op":2,
            "addr":1,
            "tp":1,
            "edn":0,
            "sc":0.01
          },
          "oilPress":{
            "op":3,
            "addr":2,
            "tp":2,
            "edn":0
          }
        },
        "cutter-2":{
          "idx":3,
          "oilGauge":{
            "op":2,
            "addr":1,
            "tp":1,
            "edn":0,
            "sc":0.01
          },
          "oilPress":{
            "op":3,
            "addr":2,
            "tp":2,
            "edn":0
          }
        }
      }
    }
  }
}
```

위 policy profile 설정으로 IoT 디바이스는 press-1, cutter-1, cutter-2 총 3대의 설비(MODBUS slave)에 연결하여 데이터를 수집할 수 있습니다. idx 가 1\~3 으로 지정되었기 때문에 실제로 MODBUS 네트워크에서도 slave ID 가 동일하게 설정되어 있어야합니다.

press-1 에서는 intTemp, mntLanp 2개의 데이터 수집 변수가 사용되고 둘의 op 모드 값은 1 과 0 이기 \
때문에 1bit 씩 읽습니다. 즉 0 과 1의 값만 가질 수 있습니다.

cutter-1, cutter-2 는 oilGauge, oilPress 라는 2개의 수집 변수가 있으며 레지스터에서 값을 읽기 때문에 \
반드시 endian 모드를 지정해야 합니다. 하지만 PLC 가 Big endian 을 지원하여 edn 이 0으로 설정되었기 \
때문에 별다른 변환 로직은 필요없습니다. \
oilGauge 는 sc 에 0.01 이 설정되어 있어서 PLC 를 읽은 값에 0.01 의 곱하여 실수로 변환한 후 서버로 \
전송해야 합니다.

* **프레스 기계의 Telemetry**

```
{
    "serviceId": 1000,
    "deviceId": "press-1",
    "timestamp": 1575084995319,
    "contents": {
        "intTemp": 62.1,
        "mntLamp": 0
    }
}
```

* **커터 기계 1의 Telemetry**

```
{
    "serviceId": 1000,
    "deviceId": "cutter-1",
    "timestamp": 1575084995319,
    "contents": {
        "oilGauge": 12.345,
        "oilPress": 12.34
    }
}
```

* **커터 기계 2의 Telemetry**

```
{
    "serviceId": 1000,
    "deviceId": "cutter-2",
    "timestamp": 1575084995319,
    "contents": {
        "oilGauge": 12.345,
        "oilPress": 12.34
    }
}
```

IoT 디바이스는 sec 필드에 적용된 5분(300초)마다 위 3개의 데이터를 PLC 로 부터 읽은 후에 서버로 \
전송해야 하며, 3개의 메시지는 개별적으로 send() 함수를 호출하도록 구현합니다. \
\
devices 바로 아래에 위치한 tag 값들이 deviceId 로 사용됩니다.
