# Modbus 서버

> 스테이션 자체의 Modbus TCP 서버: 통신 열에 있는 그 행, 켜고 끄기, 포트와 연결된 클라이언트, 태그, Logic 변수, 경보 상태를 제공하는 레지스터 맵, 클라이언트가 읽고 쓰는 것, 처음부터 끝까지의 예, 그리고 이 서버가 하지 않는 일.

**Modbus 서버**는 Modbus TCP만 말하는 PLC, HMI, 빌딩 관리 시스템이 스테이션의 태그, Logic 변수, 경보 상태를 읽고, 허용한 태그와 변수에 쓰게 해 줍니다. 데이터가 스테이션 밖으로 나가는 두 번째 길로, [내장 OPC UA 서버](connector-opcua-server)와 나란히 있습니다. 드라이버가 아닙니다. 드라이버는 장비를 읽지만, Modbus 서버는 장비에게 읽히는 쪽이어서 PLC가 접속해 오기를 기다리고 스스로는 어디에도 접속하지 않습니다. 패널도 제목 아래에서 그렇게 말합니다: "Modbus TCP로 읽고 쓰는 PLC, HMI, 빌딩 관리 시스템에 응답합니다. 연결을 받기만 하고 스스로 연결하지는 않습니다."

Modbus에는 사용자 이름도 비밀번호도 없습니다. 포트에 닿는 누구나 맵이 제공하는 모든 레지스터를 읽습니다. 그래서 서버는 꺼진 채로 시작하고, 맵의 모든 행은 읽기 전용으로 시작하며, 쓰기는 누군가 그렇게 고른 행마다 하나씩 허용됩니다. Connector 보기로 이 페이지를 읽고, Connector 구성으로 바꿉니다.

## 열 안의 행 {#the-row-in-the-column}

**Modbus 서버** 행은 통신 열 트리의 서버 폴더 안, OPC UA 서버 행 아래에 있으며, 드라이버가 플러그를 보여 주는 자리에 서버 아이콘을 보여 줍니다. 이 행은 항상 있습니다. 스테이션마다 정확히 하나이고, 만들거나 삭제하는 일이 없습니다. 검색 상자는 이름으로 이 행을 찾고, 상태 칩은 상태로 이 행을 나눕니다. 꺼져 있는 동안은 **꺼짐**, 포트가 사용 중이거나 수신 대기를 하지 못했으면 **문제**, 그 밖에는 **정상**입니다. 이름 옆에 서버가 지금 무엇을 하는지 적고, 점 하나가 그것을 요약합니다:

| 행의 표시 | 점 | 언제 |
| --- | --- | --- |
| "꺼짐" | 회색 | 서버가 꺼져 있습니다. 모든 스테이션이 이 상태로 시작합니다. |
| "포트 502에서 수신 대기 중", 이어서 연결된 클라이언트 수: "연결된 클라이언트 없음", "클라이언트 1개 연결됨", "클라이언트 3개 연결됨" | 초록 | 서버가 켜져 있고 런타임이 실행 중입니다. 포트가 열려 있습니다. |
| "포트 502은(는) 사용 중입니다" | 빨강 | 서버가 켜져 있지만 다른 프로그램이 포트를 잡고 있습니다. |
| "포트 502에서 수신 대기할 수 없습니다" | 빨강 | 서버가 켜져 있지만 Windows가 다른 이유로 포트 열기를 거부했습니다. |
| "런타임 정지" | 회색 | 서버가 켜져 있고 런타임이 멈춰 있습니다. 런타임이 다시 실행될 때까지 포트는 닫혀 있습니다. |

이 점은 서버만의 것입니다. 드라이버 폴더의 요약에도, 상태 표시줄에도 들어가지 않습니다. 그쪽은 스테이션이 읽는 장비에 관한 것이기 때문입니다. 행을 고르면 서버의 패널이 열리고, 그 머리글은 같은 점 옆에 상태를 다시 보여 줍니다. 명령은 명령 모음에 있고, 행 자체에는 메뉴가 없습니다.

## 켜고 끄기 {#turning-it-on-and-off}

| 명령 | 하는 일 | 회색이 되는 때(상황) | 그려지지 않는 때(역할) |
| --- | --- | --- | --- |
| **사용** | 도구 설명대로 서버를 켭니다: "서버를 켭니다. 런타임이 실행되는 동안 포트 502에서 수신 대기합니다." 런타임이 실행 중이면 포트가 바로 열리고, 작업 기록 줄에 "Modbus 서버를 켰습니다."가 나옵니다. | 서버가 이미 켜져 있을 때("서버가 이미 켜져 있습니다."), 실행 기록이 스테이션을 잡고 있을 때, 런타임이 멈춰 있을 때. 도구 설명이 이유를 알려 줍니다. | Connector 구성이 없을 때. |
| **사용 안 함** | 서버를 끕니다: "서버를 끕니다. 포트를 닫고 모든 클라이언트의 연결을 끊습니다." 작업 기록 줄에 "Modbus 서버를 껐습니다."가 나옵니다. | 서버가 이미 꺼져 있을 때("서버가 이미 꺼져 있습니다."), 실행 기록이 스테이션을 잡고 있을 때, 런타임이 멈춰 있을 때. | Connector 구성이 없을 때. |

켜져 있는지는 스테이션이 기억하는 설정이고, 수신 대기는 런타임을 따릅니다:

| 시점 | 서버가 하는 일 |
| --- | --- |
| 스테이션이 시작될 때 | 켜 둔 서버는 런타임이 올라오고 런타임이 재개하는 실행 기록이 돌아온 뒤에 포트를 엽니다. 꺼 둔 서버는 꺼진 채로 있습니다. |
| 포트를 열 수 없을 때 | 행에 "포트 502은(는) 사용 중입니다" 또는 "포트 502에서 수신 대기할 수 없습니다"가 나오고, 포트와 클라이언트 카드가 포트 필드 아래에 "포트 502은(는) 이 컴퓨터에서 이미 사용 중입니다. 그 포트를 쓰고 있는 것을 닫거나, 다른 포트를 고르십시오."를, 또는 같은 실패에 Windows가 밝힌 이유를 붙여 보여 줍니다. 스테이션은 서버 없이 계속 실행됩니다. 서버는 스스로 다시 시도하지 않습니다. 포트가 비면 껐다가 다시 켜거나, 다른 포트로 옮기십시오. |
| 런타임을 멈출 때 | 포트가 닫히고 모든 클라이언트의 연결이 끊어집니다. 아직 응답을 기다리던 쓰기는 기다림을 멈춥니다. 행에 "런타임 정지"가 나오고, 런타임을 다시 시작하면 포트가 다시 열립니다. |
| 애플리케이션을 닫을 때 | 다른 모든 것과 함께 포트가 닫힙니다. |

## 포트와 클라이언트 카드 {#the-port-and-clients-card}

패널의 첫 번째 카드는 **포트와 클라이언트**입니다.

| 필드 | 하는 일 | 값과 기본값 |
| --- | --- | --- |
| **TCP 포트** | 서버가 수신 대기하는 포트로, 이 컴퓨터의 모든 주소, IPv4와 IPv6에서 수신 대기합니다. 수신 대기 중에 바꾸면 이전 포트를 닫고 새 포트를 열므로 모든 클라이언트의 연결이 끊어집니다. 작업 기록 줄에 "Modbus 서버를 포트 5020(으)로 옮겼습니다."가 나옵니다. | 1에서 65535. 기본값은 Modbus 표준인 502. |
| **연결된 클라이언트** | 지금 연결된 클라이언트의 수: "연결된 클라이언트 없음", "클라이언트 1개 연결됨", "클라이언트 3개 연결됨". 누군가 연결되어 있는 동안에는 표가 각 클라이언트를 **클라이언트 주소**(그 IP 주소와 포트, 예를 들어 `192.168.0.20:51234`)와 **연결 시각**(스테이션 형식의 날짜와 시간)으로 나열합니다. | 읽기 전용. |

서버가 옮겨 갈 수 없는 포트는 무엇이든 저장하기 전에 거부됩니다. 이유가 필드 아래에 나오고, 필드는 지금 쓰는 포트로 돌아가며, 서버는 있던 자리에 그대로 있습니다. 거부되는 입력은 여섯 가지입니다: 범위를 벗어난 수("포트는 1에서 65535 사이여야 합니다."), [원격 접근](remote-access)에서 정한 웹 서버의 포트("포트 N은(는) 이미 웹 서버가 쓰고 있습니다. 다른 포트를 고르거나, 웹 서버를 먼저 옮기십시오."), [에이전트](agent) 페이지에서 에이전트 접근에 정한 포트, 에이전트 접근이 켜져 있든 꺼져 있든("포트 N은(는) 이미 에이전트 접근이 쓰고 있습니다. 다른 포트를 고르거나, 에이전트 접근을 먼저 옮기십시오."), [구성](connector-opcua-configuration)에 있는 OPC UA 서버의 포트, 실행 중이든 멈춰 있든("포트 N은(는) 이미 OPC UA 서버가 쓰고 있습니다. 다른 포트를 고르거나, 서버를 먼저 옮기십시오."), Connector 페이지의 [MQTT 브로커](connector-mqtt-broker) 포트, 브로커가 켜져 있든 꺼져 있든("포트 N은(는) 이미 MQTT 브로커가 쓰고 있습니다. 다른 포트를 고르거나, MQTT 브로커를 먼저 옮기십시오."), 그리고 이 컴퓨터에서 지금 다른 무언가가 잡고 있는 포트("포트 N은(는) 이 컴퓨터에서 이미 사용 중입니다. 그 포트를 쓰고 있는 것을 닫거나, 다른 포트를 고르십시오."). 다른 네 포트 필드도 이번에는 이 서버의 포트를 거부합니다. 이유는 필드가 다음에 받아들이는 포트까지, 또는 행을 다시 고를 때까지 필드 아래에 남습니다.

카드 아래에는 이런 안내가 있습니다: "어떤 슬레이브 ID에도 응답합니다. 다른 컴퓨터가 이 서버에 닿으려면 Windows 방화벽이 TCP 포트 502의 수신을 허용해야 합니다. 허용할지는 사용자가 정하며, 이 스테이션이 스스로 허용하는 일은 없습니다."

서버는 한 번에 최대 16개의 클라이언트를 받습니다. 17번째 연결은 열리자마자 닫히고 진단 로그에 그렇게 기록됩니다. 연결을 닫지 않고 사라진 클라이언트(전원이 꺼진 PLC, 뽑힌 케이블)는 TCP 킵얼라이브로 찾아내며, 그 자리는 1분 30초쯤 안에 비워집니다.

### Windows 방화벽 {#windows-firewall}

안내 아래에서 점 하나와 문장 하나가 스테이션 자신의 인바운드 규칙이 이 포트를 들여보내는지 알립니다. [클라이언트 접근](connector-opcua-client-access#windows-firewall) 카드가 OPC UA 포트에 대해 알리는 것과 같으며, 들여보내지 않을 때 조작 하나를 내놓습니다.

| 상태 | 글 |
| --- | --- |
| 허용됨(초록) | "개인 및 도메인 네트워크에서 포트 N의 인바운드 Modbus 연결이 허용되어 있습니다." |
| 설정되지 않음(호박색) | "포트 N을(를) 허용하는 이 앱의 규칙이 없어 네트워크 클라이언트가 차단될 수 있습니다." **로컬 네트워크 허용** 단추가 그려집니다. |
| 지원하지 않음(회색) | "이렇게 실행한 앱은 Windows 방화벽을 관리할 수 없습니다. 필요하면 운영 체제에서 인바운드 접근을 설정하십시오." 개발이나 이동식이나 데모 실행, 또는 설치된 애플리케이션이 아닌 모든 실행입니다. |
| 알 수 없음(회색) | "Windows 방화벽 상태를 확인하지 못했습니다." |

| 명령 | 하는 일 | 회색이 되는 때(상황) | 그려지지 않는 때(역할) |
| --- | --- | --- | --- |
| **로컬 네트워크 허용** | 앱 자신의 도우미를 권한을 올려 실행하므로 Windows가 관리자 승인을 묻고, 이 포트에 대한 규칙을 개인과 도메인 프로필, 로컬 서브넷, 이 프로그램으로 좁혀 더합니다. 그동안 "적용하는 중…"으로 읽히고, 작업 기록 줄에 "Modbus 포트 N을(를) 위해 Windows 방화벽 접근을 요청하는 중…", 그다음 "이제 Windows 방화벽이 포트 N의 Modbus 연결을 허용합니다."이거나 상태 글이 오류로 나옵니다. 승인 창을 취소하면 상태는 설정되지 않음으로 남습니다. | 다른 기기의 브라우저에서("방화벽 규칙은 스테이션의 데스크톱 앱에서만 적용할 수 있습니다."), 실행 기록이 스테이션을 잡고 있거나 런타임이 멈춰 있을 때, 그리고 규칙을 적용하는 동안("방화벽 규칙을 적용하는 중입니다."). | Connector 구성이 없을 때. |

상태는 카드가 열릴 때와 포트가 바뀔 때 다시 읽습니다. 그래서 새 포트는 허용하기 전까지 설정되지 않음으로 나옵니다. 스테이션이 스스로 규칙을 더하는 일은 없습니다.

## 레지스터 맵 {#the-register-map}

두 번째 카드인 **레지스터 맵**이 서버가 제공하는 것입니다. 각 행은 태그 하나, Logic 변수 하나 또는 경보 상태 하나를 클라이언트가 지정하는 레지스터나 코일에 놓습니다. 어떤 행도 덮지 않는 레지스터는 클라이언트에게 존재하지 않습니다. 카드에는 이렇게 쓰여 있습니다: "각 행은 태그 하나, Logic 변수 하나 또는 경보 상태 하나를 레지스터 하나에서 제공합니다. 쓰기를 허용할 때까지 행은 읽기 전용이며, 경보를 제공하는 행은 항상 읽기 전용입니다. 허용하면 클라이언트의 쓰기는 다른 모든 쓰기와 같은 경로를 거치고, 클라이언트 주소와 함께 진단 로그에 기록됩니다."

### 표 {#the-table}

| 열 | 보여 주는 것 |
| --- | --- |
| **레지스터 종류** | 홀딩 레지스터 (4xxxx), 입력 레지스터 (3xxxx), 코일 (0xxxx), 디스크리트 입력 (1xxxx). 모두 매뉴얼의 주소가 그 종류에 붙이는 첫 숫자를 함께 적습니다. |
| **레지스터** | 행이 시작하는 레지스터(0부터 셉니다)와 행이 차지하는 마지막 레지스터. 레지스터 하나짜리 행은 `0`, Float는 `0-1`. |
| **매뉴얼상** | 같은 자리를 클라이언트의 매뉴얼이나 PLC 도구가 쓰는 표기로 보여 줍니다. 종류의 첫 숫자 뒤에 1부터 센 레지스터를 붙입니다. 홀딩 레지스터 0은 `40001`, 거기 있는 Float는 `40001에서 40002까지`, 9999를 넘으면 여섯 자리입니다(홀딩 레지스터 9999는 `410000`). |
| **데이터 형식** | Boolean, Int16, UInt16, Int32, Float, String. |
| **접근** | 읽기 전용, 또는 읽기와 쓰기. |
| **신호원** | 선택 목록이 붙이는 이름으로 나타낸 신호원. |
| **현재 값** | 클라이언트가 지금 그 자리에서 읽는 값. 그 행을 읽는 클라이언트가 예외 04를 받을 때는 줄표입니다. |

행은 종류 순서(홀딩 레지스터, 입력 레지스터, 코일, 디스크리트 입력)로, 그다음 레지스터 순서로 나열됩니다. 행이 없으면 "아직 제공하는 레지스터가 없습니다. 새 행으로 추가하십시오."가 나옵니다. 행을 클릭하면 그 행이 선택되고 표 아래에 그 필드가 열립니다. 어느 행이 선택되어 있는지는 지금 쓰는 창마다 따로입니다.

표를 늘리는 명령과 행을 끝내는 명령은 카드 제목 오른쪽에 있습니다:

| 명령 | 하는 일 | 회색이 되는 때(상황) | 그려지지 않는 때(역할) |
| --- | --- | --- | --- |
| **새 행** | 어떤 행도 쓰지 않는 첫 홀딩 레지스터에 행을 추가합니다. Float, 두 순서 확인란은 켜짐, 읽기 전용, 아직 신호원 없음. 그 행이 선택되고 작업 기록 줄에 "홀딩 레지스터 (4xxxx) 40001에서 40002까지에 행을 추가했습니다."가 나옵니다. 홀딩 레지스터가 남아 있지 않으면 "새 행에 쓸 수 있는 홀딩 레지스터가 남아 있지 않습니다."가 나옵니다. | 실행 기록이 스테이션을 잡고 있을 때, 또는 런타임이 멈춰 있을 때. | Connector 구성이 없을 때. |
| **행 삭제**(위험을 나타내는 색) | 표에서 선택된 행을 확인 없이 바로 삭제합니다. 도구 설명이 그 행을 가리키고("홀딩 레지스터 (4xxxx) 40001에서 40002까지의 행 삭제"), 작업 기록 줄에 "홀딩 레지스터 (4xxxx) 40001에서 40002까지의 행을 삭제했습니다."가 나옵니다. 그 행이 제공하던 태그나 변수는 건드리지 않습니다. 그 뒤로 그 레지스터를 읽는 클라이언트는 예외 02를 받습니다. | 선택된 행이 없을 때("삭제할 행을 레지스터 맵에서 고르십시오."), 실행 기록이 스테이션을 잡고 있을 때, 또는 런타임이 멈춰 있을 때. | Connector 구성이 없을 때. |

### 행의 필드 {#a-rows-fields}

행은 [Modbus 태그](driver-modbus-tcp#tag-addressing)와 똑같은 필드와 규칙을 가집니다. 선의 한쪽 끝에서 받아들여진 포인트는 다른 쪽 끝에서도 받아들여집니다. 각 필드는 바뀌는 순간 저장되며(숫자는 필드를 떠나거나 Enter를 누를 때, 선택은 고를 때, 확인란은 켤 때), 클라이언트는 연결이 끊기지 않은 채 다음 요청부터 새 행으로 응답을 받습니다.

| 필드 | 하는 일 | 값과 기본값 |
| --- | --- | --- |
| **레지스터 종류** | 행이 Modbus의 어느 표에서 응답하는지. 코일이나 디스크리트 입력을 고르면 형식이 Boolean이 되고, 입력 레지스터나 디스크리트 입력을 고르면 행이 읽기 전용이 됩니다. | 홀딩 레지스터 (4xxxx), 입력 레지스터 (3xxxx), 코일 (0xxxx), 디스크리트 입력 (1xxxx). 새 행은 홀딩 레지스터. |
| **레지스터 번호 (0부터)** | 행이 시작하는 자리로, 요청 안에서 오가는 그대로의 번호입니다. 홀딩 레지스터 0은 고전적인 표기에서 40001이라고 부르는 것입니다. 옆의 **매뉴얼상의 주소**가 **매뉴얼상** 열처럼 행을 그 표기로 읽어 줍니다. | 0에서 65535. 새 행은 비어 있는 첫 홀딩 레지스터. |
| **데이터 형식** | 값을 놓는 방식으로, Modbus 태그와 같은 방식입니다([값 형식과 개수](driver-modbus-tcp#value-types-and-quantities)). Boolean은 레지스터 하나(1 또는 0)나 비트 하나, Int16과 UInt16은 레지스터 하나, Int32와 Float는 둘, String은 UTF-8로 레지스터마다 2바이트입니다. 코일과 디스크리트 입력은 Boolean만 담습니다. 레지스터 행에서 Boolean을 고르면 디스크리트 입력(읽기 전용)이나 코일(읽기와 쓰기)로 옮겨지고, 코일이나 디스크리트 입력에서 숫자를 고르면 입력 레지스터나 홀딩 레지스터로 옮겨집니다. Modbus 태그의 패널과 같은 동작입니다. | Boolean, Int16, UInt16, Int32, Float, String. 새 행은 Float. |
| **길이 (레지스터 수)** | String일 때만 그려집니다. 텍스트가 차지하는 레지스터 수입니다. | 비우면 4. 1에서 65535. |
| **최상위 바이트 먼저 (MSB first, 빅 엔디언)** | 각 레지스터의 어느 바이트를 먼저 보내는지. 괄호 안은 매뉴얼이 이 설정에 쓰는 말입니다. String에서는 그려지지 않고, 코일이나 디스크리트 입력에서도 그려지지 않습니다. | 켜짐. |
| **최상위 워드 먼저 (상위 워드 먼저)** | 두 레지스터 중 어느 쪽이 위쪽 절반을 담는지. Int32와 Float에서만 그려집니다. 두 확인란이 켜져 있으면 Float는 사양의 순서대로 `AB CD`로 보내집니다([바이트와 워드 순서](driver-modbus-tcp#byte-and-word-order)). 확인란 아래의 **값의 바이트 순서**가 두 상자가 만드는 배치(ABCD, CDAB, BADC, DCBA)를 보여 줍니다. | 켜짐. |
| **접근** | 클라이언트가 그 행에 쓸 수 있는지. 입력 레지스터나 디스크리트 입력에서는 **읽기와 쓰기**가 목록에 남은 채 회색이 되고, 도구 설명이 "쓰기를 받는 것은 홀딩 레지스터와 코일뿐입니다."라고 말합니다. 경보를 제공하는 행에서도 회색이 되며 "경보를 제공하는 행은 읽기 전용입니다."라고 말합니다. | **읽기 전용** 또는 **읽기와 쓰기**. 새 행은 읽기 전용. |
| **신호원** | 행이 제공하는 것. 목록은 "(없음)"으로 시작하고, 이어서 사용 중인 장치의 사용 중인 태그가 모두 드라이버, 장치, 태그 자신의 이름으로("태그 · Simulated · Oven · Temperature"), 그다음 값을 담는 Logic 변수(부울, 정수, 실수, 텍스트, 날짜와 시간)가 모두 그 이름으로("변수 · Setpoint", 폴더에 넣은 변수는 폴더와 이름으로 "변수 · Oven / Setpoint"이어서 같은 이름의 변수도 구별됩니다) 나옵니다. 이어서 모든 경보가 같은 방식으로 이름과 함께 나옵니다("경보 · Oven / High temperature"). 경보를 제공하는 행은 항상 읽기 전용이며, **제공하는 경보 상태**가 정한 상태를 제공합니다. 행은 신호원을 그 신원으로 붙들고 있으므로, 태그나 변수의 이름을 바꾸거나 장치를 폴더에 넣어도 행은 같은 포인트에 그대로 있습니다. 목록이 더 이상 내놓지 않는 신호원은 선택된 채로 남고 이름도 그대로 나옵니다. 사용 안 함인 태그는 "태그 · Simulated · Oven · Temperature (사용 안 함)", 사용 안 함인 장치의 태그는 "태그 · Simulated · Oven · Temperature (장치 사용 안 함)"로 나오며, 없어진 태그나 없어진 변수라는 이름에 그 신원이 붙는 것은 더 이상 없는 신호원뿐입니다. 태그의 [사용하는 곳](connector-tags#the-used-by-card) 카드는 그 태그를 제공하는 행을 나열하고, 모두 바꾸기는 그 행을 고른 태그로 옮깁니다. | 새 행은 "(없음)". |
| **제공하는 경보 상태** | 경보를 제공하는 행에 나옵니다. 클라이언트가 경보의 어떤 상태를 읽을지 정합니다. **발생 (Boolean)**는 조건이 성립하는 동안 1, **확인됨 (Boolean)**는 확인을 기다리는 것이 없는 동안 1, **상태 (숫자)**는 상태를 숫자로 나타낸 것입니다. | 새 경보 행은 발생 (Boolean). |

필드 아래에서는 세 줄이 행을 다시 읽어 줍니다. 놓인 자리("레지스터 0을(를) 차지합니다." 또는 "레지스터 0부터 1까지 차지합니다."), 클라이언트가 할 수 있는 일("클라이언트는 이 행을 읽을 수 있지만 쓸 수는 없습니다." 또는 "클라이언트는 이 행을 읽고 쓸 수 있습니다. 쓰기는 그 주소와 함께 진단 로그에 기록됩니다.", 경보를 제공하는 행에서는 "클라이언트는 이 경보 상태를 읽기만 하고 쓰지 않습니다. 숫자로는 0이 정상, 1이 발생·미확인, 2가 발생·확인됨, 3이 복귀·미확인입니다."), 그리고 클라이언트가 지금 그 자리에서 읽는 값("현재 값: 21.5")입니다. 그 행을 읽는 클라이언트가 값을 받지 못할 때는 이유가 이어집니다:

| 패널의 표시 | 언제 |
| --- | --- |
| "이 행은 아직 아무것도 제공하지 않습니다. 신호원을 고르십시오." | 신호원을 고르지 않았습니다. |
| "이 행의 신호원이 더 이상 없습니다. 이 행을 읽는 클라이언트에는 예외 04로 응답합니다." | 태그나 변수가 삭제되었습니다. |
| "신호원에 지금 유효한 값이 없습니다. 값이 다시 생길 때까지 이 행을 읽는 클라이언트에는 예외 04로 응답합니다." | 태그에 유효한 값이 없거나(장치가 응답하지 않고 태그가 대체값을 내놓지 않음, 태그나 그 장치가 사용 안 함, 한계 이상으로 품질이 나빠짐), 변수가 빈 값입니다. |
| "신호원의 값이 이 행의 형식에 맞지 않습니다. 이 행을 읽는 클라이언트에는 예외 04로 응답합니다." | 이 형식의 행이 담을 수 없는 값입니다. 정수 행의 NaN, 또는 행의 레지스터보다 긴 텍스트. |

### 행이 거부하는 것 {#what-a-row-refuses}

서버의 규칙이 거부한 변경은 저장되지 않습니다. 이유가 행의 필드 위에 빨간색으로 나오고, 고칠 수 있도록 변경은 화면에 남으며, 변경이 받아들여질 때까지 행은 이전에 제공하던 것을 계속 제공합니다. 다른 행은 영향을 받지 않습니다.

| 패널의 표시 | 언제 |
| --- | --- |
| "레지스터 번호는 0에서 65535 사이여야 합니다." | 프로토콜 범위를 벗어난 레지스터. |
| "Modbus 포인트가 담는 형식은 Boolean, Int16, UInt16, Int32, Float, String입니다." | 이 여섯 가지 밖의 형식. 에이전트만 요청할 수 있습니다. |
| "코일과 디스크리트 입력은 Boolean만 담습니다." | 코일이나 디스크리트 입력의 숫자. 에이전트만 요청할 수 있습니다. |
| "텍스트는 레지스터 1개에서 65535개를 차지합니다." | 범위를 벗어난 String 길이. |
| "레지스터 65535부터 65536까지는 레지스터 65535를 넘어갑니다." | 마지막 레지스터 뒤에서 끝나게 될 행. 여기서는 65535에 놓인 Float입니다. |
| "레지스터 40001부터 40002까지는 홀딩 레지스터 (4xxxx) 40002의 행과 겹칩니다." | 같은 종류의 두 행이 한 레지스터를 나눠 쓰는 경우. |
| "입력 레지스터와 디스크리트 입력은 클라이언트에게 읽기 전용입니다. 쓰기를 받는 것은 홀딩 레지스터와 코일뿐입니다." | 입력 종류에서 쓰기를 허용한 경우. 에이전트만 요청할 수 있습니다. |
| "쓰기를 받는 행에는 쓰기가 갈 태그나 변수가 있어야 합니다." | 신호원보다 먼저 읽기와 쓰기를 고른 경우, 또는 쓰기를 허용한 행의 신호원을 "(없음)"으로 되돌린 경우. 신호원을 먼저 고르십시오. |
| "이 신호원에는 쓸 수 없습니다. 쓰기 접근이 있는 태그나 내부 값 변수를 고르거나, 행을 읽기 전용으로 두십시오." | 신호원이 읽기 전용 태그, 쓰지 않는 드라이버의 태그, 수식 변수, 또는 더 이상 없는 신호원인 행에서 쓰기를 허용한 경우. |
| "신호원은 텍스트나 날짜를 담고 있습니다. 이를 담을 수 있는 것은 String 행뿐입니다." | 텍스트나 날짜 태그, 또는 텍스트나 날짜와 시간 변수를 다른 형식의 행에 둔 경우. |
| "경보를 제공하는 행은 읽기 전용입니다. 클라이언트는 경보 상태를 읽기만 하고 쓰지 않습니다." | 경보를 제공하는 행에 읽기와 쓰기를 요청한 경우로, 에이전트만 요청할 수 있습니다. |
| "레지스터 맵의 이 행은 더 이상 없습니다. 다시 불러온 뒤 다시 시도하십시오." | 편집하는 동안 다른 창이나 에이전트가 그 행을 삭제했습니다. |

실행 기록이 스테이션을 잡고 있거나 런타임이 멈춰 있는 동안에는 페이지의 다른 모든 변경처럼 변경이 거부되고, 이유가 작업 기록 줄에 나오며("유닛이 실행 중인 동안에는 구성이 잠깁니다.", "런타임 정지"), 아무것도 저장되지 않습니다.

## 클라이언트가 읽고 쓰는 것 {#what-a-client-reads-and-writes}

서버는 어떤 슬레이브 ID에도 응답하고, 물어 온 슬레이브 ID 그대로 응답합니다. TCP에서는 스테이션을 IP 주소로 가리키기 때문입니다. 레지스터 번호는 0부터 센 그대로 오갑니다. 1부터 세거나 고전적인 표기를 쓰는 PLC 도구는 그 차이를 스스로 더합니다:

| 레지스터 종류 | 읽을 때의 기능 | 쓸 때의 기능 | 고전적인 표기의 레지스터 0 |
| --- | --- | --- | --- |
| 홀딩 레지스터 (4xxxx) | 3 | 6(레지스터 하나) 또는 16(여러 개) | 40001 |
| 입력 레지스터 (3xxxx) | 4 | 없음: 읽기 전용 | 30001 |
| 코일 (0xxxx) | 1 | 5(코일 하나) 또는 15(여러 개) | 00001 |
| 디스크리트 입력 (1xxxx) | 2 | 없음: 읽기 전용 | 10001 |

다른 기능 코드에는 예외 01로 응답합니다. 요청 하나로 읽을 수 있는 것은 최대 125개 레지스터나 2000개의 코일 또는 입력이고, 쓸 수 있는 것은 최대 123개 레지스터나 1968개 코일입니다.

읽기에는 전부를 돌려주거나 아무것도 돌려주지 않습니다. 각 값은 스테이션의 현재 상태에서 읽으며, 클라이언트를 위해 계산하는 일은 없습니다. 태그는 공학값, 곧 [값 변환 경로](connector-value-pipeline)를 거친 뒤 스테이션이 보여 주는 값을 내놓고, 변수는 그 값을, 경보는 행이 정한 상태를 내놓습니다. 값은 스테이션의 하나뿐인 변환으로 행의 형식에 맞춰 바뀝니다. 정수 행에서는 실수를 가장 가까운 정수로 반올림하고(절반은 0에서 먼 쪽으로) 형식의 한계에서 멈추므로, Int16 행은 40000에 대해 32767을 내놓습니다. 읽기는 레지스터 두 개짜리 행의 일부만 요청해도 됩니다. 요청한 레지스터를, 인코딩된 값이 담는 그대로 받습니다. 신호원에 유효한 값이 없는 행에 서버가 남겨 둔 마지막 값을 내놓는 일은 없습니다. 레지스터는 품질을 담지 않으므로, 오래된 숫자를 읽은 PLC는 스테이션이 이미 현재 값이 아니라고 아는 값으로 움직이게 되기 때문입니다. 클라이언트는 대신 예외 04를 받고, 어떻게 할지는 그 PLC 자신의 통신 이상 처리가 정합니다. 그 대가로, 값이 없는 행 하나가 그 행을 걸치는 블록 읽기를 실패하게 만들며, 어느 행인지는 맵의 현재 값 열이 알려 줍니다. 스스로 대체값을 내놓는 태그([올바르지 않은 읽기](connector-value-pipeline) 단계에서 정한, 붙들어 둔 마지막 유효값이나 정해 둔 숫자)는 유효한 값을 계속 가지며, 그 대체값은 다른 값처럼 제공됩니다.

클라이언트의 쓰기는 다른 모든 쓰기와 같은 하나뿐인 쓰기 통로를 지납니다([Logic](logic#writing-values) 참고). 태그는 그 장치의 전달 대기열을 거치며 공학값이 태그의 변환을 거꾸로 거치고, 내부 값 변수는 Logic의 대기열을 거칩니다. 신호원 잠금이 적용되므로, 실행 중인 Process의 레시피나 인터록이 잡고 있는 신호원은 거부됩니다. 쓰기 통로는 각 값을 이전 값에서 새 값으로 `Modbus client 192.168.0.20:51234`라는 출처 아래 진단 로그에 기록하고, 응답은 각 값의 수령 확인을 기다립니다. 태그라면 수령 확인은 장비 자신의 응답입니다.

- 쓰기는 행 전체를 덮습니다. Float 행의 레지스터 하나만 쓰면 거부되므로, Float는 기능 16과 개수 2로 씁니다.
- 쓰기가 닿는 모든 행은 값을 하나라도 보내기 전에 맵과 대조됩니다. 어떤 행에도 없는 레지스터나 읽기 전용 행이 있으면 요청 전체가 거부되고, 아무것도 쓰이지 않습니다.
- 그다음 값은 레지스터 순서대로 각 신호원으로 갑니다. 쓰기 통로가 그중 하나에 내린 거부가 클라이언트가 받는 응답이 되지만, 같은 요청의 다른 행도 이미 넘겨졌습니다.

| 예외 | 언제 |
| --- | --- |
| 01(잘못된 기능) | 1, 2, 3, 4, 5, 6, 15, 16이 아닌 기능 코드. |
| 02(잘못된 데이터 주소) | 어떤 행에도 없는 레지스터, 읽기 전용 행이나 행 일부에 대한 쓰기, 65535를 넘는 범위, 쓰기 통로가 쓸 수 없다고 하는 신호원. |
| 03(잘못된 데이터 값) | 범위를 벗어난 개수나 형식이 깨진 요청, 켜짐이나 꺼짐이 아닌 값으로 쓴 코일, 신호원이 받을 수 없는 값. |
| 04(서버 장치 고장) | 유효한 값이 없는 신호원, 더 이상 없는 신호원, 행이 담을 수 없는 값. 장비가 거부했거나 응답하지 않은 쓰기, 또는 연결되지 않은 장치. |
| 06(서버 장치 사용 중) | 실행 중인 Process가 잡고 있는 신호원, 또는 멈추는 중인 런타임. |

## 처음부터 끝까지의 예 {#a-worked-example}

PLC가 스테이션에서 오븐의 온도와 문 스위치를 읽고, 오븐의 설정값인 내부 값 변수 `Variables.Oven.Setpoint`에 씁니다. 패널에서는:

1. **Modbus 서버** 행을 고르고 **새 행**을 누릅니다. **레지스터 종류**를 입력 레지스터 (3xxxx)로(레지스터는 0 그대로), **신호원**를 "태그 · Simulated · Oven · Temperature"로 정합니다. 행에 "레지스터 0부터 1까지 차지합니다."가 나오고, **매뉴얼상의 주소**는 30001에서 30002까지가 되며, 현재 값 열에 온도가 나옵니다.
2. **새 행**을 다시 누르고, **레지스터 종류**를 디스크리트 입력 (1xxxx)으로 정한 뒤(형식은 Boolean이 되고 레지스터는 0 그대로) 문 스위치 "태그 · Simulated · Oven · DoorOpen"을 고릅니다.
3. **새 행**을 세 번째로 누릅니다. 행은 홀딩 레지스터 0에 Float로 놓입니다. 먼저 "변수 · Oven / Setpoint"를 고르고, 그다음 **접근**을 **읽기와 쓰기**로 정합니다. 행에 "클라이언트는 이 행을 읽고 쓸 수 있습니다. 쓰기는 그 주소와 함께 진단 로그에 기록됩니다."가 나옵니다.
4. 명령 모음의 **사용**을 누릅니다. 행에 "포트 502에서 수신 대기 중"이 나옵니다. 다른 컴퓨터의 PLC를 위해 그다음 포트와 클라이언트 카드의 **로컬 네트워크 허용**을 누릅니다.

두 순서 확인란이 켜져 있을 때(Float는 빅 엔디언, 최상위 워드 먼저, `ABCD`) PLC가 요청하는 것:

| 값 | 기능 | 주소 | 개수 | 고전적인 표기 |
| --- | --- | --- | --- | --- |
| 온도(읽기) | 4 | 0 | 2 | 30001-30002 |
| 문 스위치(읽기) | 2 | 0 | 1 | 10001 |
| 설정값(쓰기) | 16 | 0 | 2 | 40001-40002 |
| 설정값(다시 읽기) | 3 | 0 | 2 | 40001-40002 |

같은 일을 에이전트로, Connector 범위의 도구 네 개로 하면:

```text
modbus_map_upsert(source: "Connector.Simulated.Oven.Temperature", registerClass: "InputRegister", register: 0, type: "Float")
modbus_map_upsert(source: "Connector.Simulated.Oven.DoorOpen", registerClass: "DiscreteInput", register: 0)
modbus_map_upsert(source: "Variables.Oven.Setpoint", registerClass: "HoldingRegister", register: 0, type: "Float", access: "ReadAndWrite")
modbus_server_configure(enabled: true)
```

에이전트가 만든 행은 `access`가 달리 말하지 않는 한 읽기 전용이고, 형식을 주지 않은 코일이나 디스크리트 입력은 Boolean입니다. `modbus_server_status`는 서버 전체를 다시 읽습니다. 상태, 클라이언트, 그리고 각 행과 클라이언트가 지금 그 자리에서 읽는 값입니다. 도구는 [도구를 범위별로](agent-tools#connector--configure)에 설명되어 있습니다.

## 권한과 잠금 {#permissions-and-locks}

| 권한 | 여기서 여는 것 |
| --- | --- |
| Connector 보기 | 행, 패널, 레지스터 맵을 읽기 전용으로. 카드는 "Connector를 바꾸려면 'Connector 구성' 권한이 필요합니다." 안내 아래에서 조작할 수 없게 되고, 동사는 하나도 그려지지 않습니다. 사용, 사용 안 함, 새 행, 행 삭제 모두 나오지 않습니다. |
| Connector 구성 | 페이지의 모든 것. 서버 켜고 끄기, 포트, 맵의 각 행. |

실행 기록이 스테이션을 잡고 있거나 런타임이 멈춰 있는 동안에는, [Connector](connector#engineering-boundaries) 페이지의 어디서나 그렇듯 동사는 그려진 채 이유와 함께 회색이 되고, 모든 변경이 거부됩니다. 패널을 읽는 일과 클라이언트가 서버를 읽는 일은 계속됩니다. 에이전트는 [에이전트 접근](agent-areas)의 Connector 구성 범위를 통해서만 서버를 바꾸며, 에이전트가 한 변경은 모두 에이전트의 것으로 기록됩니다.

## 진단 로그 {#the-journal}

서버는 이벤트의 [콘솔](events-console)에 Server 범위, 구성 요소 Modbus server로 기록합니다. 수신 대기를 시작하고 멈춘 때, 열지 못한 포트, 이미 16개 클라이언트에 응답하고 있어서 닫은 연결, 그리고 거부한 모든 요청과 그 예외 코드입니다. 맵에 없는 레지스터를 주기마다 묻는 PLC처럼 반복되는 거부는 처음 한 번 기록하고, 이어지는 동안 몇 분마다 요약하며, 클라이언트가 다시 응답을 받으면 한 줄로 마무리합니다. 연결과 연결 끊김은 세부 사항이라 [상세 수집](events-console#detailed-capture)을 위해 남겨 둡니다. 서버 설정이나 맵의 변경도 페이지에서 왔든 에이전트에서 왔든 기록됩니다. 클라이언트의 쓰기가 무엇을 했는지는 쓰기 통로 자신의 줄이며, 출처는 `Modbus client <address>:<port>`입니다.

## 서버가 하지 않는 일 {#what-the-server-does-not-do}

- MBAP 헤더를 쓰는 Modbus TCP만 제공합니다. 시리얼 회선의 Modbus RTU나 ASCII 서버는 없고, TCP 위의 RTU 프레임도 없습니다.
- Windows 방화벽을 스스로 열지 않으며(누를 때의 **로컬 네트워크 허용**만 엽니다), 네트워크 카드 하나에서만 수신 대기할 수도 없습니다. 컴퓨터의 모든 주소에서, 포트 하나로 수신 대기합니다.
- 어떤 슬레이브 ID에도 같은 하나의 맵으로 응답합니다. 맵이 다른 여러 유닛을 둘 수는 없습니다.
- Modbus에 없기 때문에 사용자도, 비밀번호도, 암호화도 없습니다. 포트에 닿는 누구나 모든 행을 읽고, 쓰기를 허용한 모든 행에 씁니다. 클라이언트가 쓸 필요가 없는 행은 읽기 전용으로 두고, 상관없는 네트워크에는 포트를 닫아 두십시오.
- 스스로는 어디에도 접속하지 않습니다. Modbus 장비를 읽으려면 [Modbus TCP](driver-modbus-tcp)나 [Modbus RTU](driver-modbus-rtu) 드라이버 아래에 추가하십시오.
- 자기 레지스터를 갖지 않습니다. 어떤 행도 덮지 않는 레지스터는 존재하지 않으며, 클라이언트가 쓰는 것은 모두 태그나 변수에 들어갑니다.
- 빠진 값 대신 내놓을 자기 값을 남겨 두지 않으며, 다른 기능 코드(Read/Write Multiple Registers, Mask Write Register, 장치 식별)도 실행하지 않습니다. 그런 요청에는 예외 01로 응답합니다.
- 맵에는 따로 가져오기나 내보내기가 없습니다. 스테이션의 구성과 함께 [백업](settings-backups)에 담겨 옮겨집니다.
