콘텐츠로 이동
무료 평가 시작

실패 진단과 문의

통신이 실패하면 먼저 어느 층에서 실패했는지 찾고, 재현 조건을 줄인 뒤, 같은 조건에서 증거를 모읍니다. 이 튜토리얼은 응답하지 않는 TCP 상대를 만들어 시간 초과를 재현하고, 진단 로그로 원인을 확인합니다.

  • 실패를 SDK 로딩, 구성, 연결, 거래, 장비 의미 가운데 한 층으로 좁힙니다.
  • PDCOMM_LOG 환경 변수로 JSON Lines 진단 로그를 켜고, 시간 초과 기록을 읽습니다.
  • 실행 중에 로그를 켜고 끄는 API와 명령을 씁니다.
  • 문의에 필요한 정보를 모읍니다.
  • 빠른 시작의 SDK, 예제, C++ 콘솔
  • Python 3, jq
  • 터미널 두 개

아래 순서로 확인합니다. 앞 층이 실패하면 뒤 층은 볼 필요가 없습니다.

층 확인 방법 이 층의 실패 예
SDK 로딩 {"action":"capabilities"}가 응답하는가 DllNotFoundException, 라이브러리를 찾지 못함
구성 configure 결과가 ok인가 Invalid endpoint configuration, missing field `port`
연결 connect 결과의 connected가 true인가 Io(ConnectionRefused, ...), Io(AddrInUse, ...)
거래 request 결과가 오는가 Timeout, Closed, Protocol(...)
장비 의미 status가 success이고 값이 매뉴얼과 맞는가 예외 01·02·03, 주소가 한 칸 어긋난 값
  • ok가 true여도 Modbus 예외일 수 있습니다. status를 함께 봅니다.
  • source가 local인 결과는 통신 결과가 아닙니다. 실제 장비와의 통신은 wire 결과로 판단합니다.
  • 브로드캐스트와 청취 전용 모드의 no_response는 의도된 무응답입니다.
  1. 새 프로세스에서 capabilities와 state를 기록합니다.
  2. 가능하면 같은 PC의 127.0.0.1에서 기능 06과 03으로 먼저 확인합니다. 다른 장비와 포트를 공유하지 않습니다.
  3. 실패하는 요청 하나만 남기고, 정확한 PDU, unit, response, status, source를 기록합니다.
  4. 클라이언트와 서버의 trace를 각각 읽어 순서를 맞춥니다.
  5. 프로세스의 종료 코드를 기록합니다.

쓰기 요청이 시간 초과되면 상대가 이미 처리했을 수 있습니다. 다시 쓰기 전에 값을 읽어 확인합니다. PDComm은 쓰기를 자동으로 다시 보내지 않습니다.

3. 응답하지 않는 상대를 만듭니다

섹션 제목: “3. 응답하지 않는 상대를 만듭니다”

터미널 A에서 연결은 받지만 아무것도 보내지 않는 TCP 상대를 실행합니다. 30초 뒤 저절로 끝납니다.

터미널 창
python3 -c "import socket,time; s=socket.create_server(('127.0.0.1',1503)); c,_=s.accept(); time.sleep(30)"

4. 진단 로그를 켜고 재현합니다

섹션 제목: “4. 진단 로그를 켜고 재현합니다”

터미널 B에서 환경 변수로 로그를 켜고 C++ 콘솔을 시작합니다.

터미널 창
mkdir -p logs
PDCOMM_LOG=debug PDCOMM_LOG_FILE=logs/client.jsonl ./build/modbus-tutorial/pdcomm-modbus-tutorial
{"action":"configure","config":{"transport":"tcp","role":"client","address":"127.0.0.1:1503","port":"","unit":1,"baud":19200,"parity":"even","stop_bits":1,"timeout_ms":1000}}
{"action":"connect"}
{"action":"request","pdu":"0300000001"}
{"action":"logging_flush"}
{"action":"logging_status"}

마지막에 quit를 입력해 콘솔을 끝냅니다. request는 1초 뒤 Command error: {"error":"Timeout","ok":false}로 실패합니다. logging_status의 written은 기록한 줄 수이고, dropped, write_errors, configuration_errors가 0이면 기록이 빠지지 않았습니다.

환경 변수 뜻 기본값
PDCOMM_LOG error, warn, info, debug, trace 또는 off 꺼짐
PDCOMM_LOG_FILE 로그 파일 경로. 없으면 표준 오류에 씁니다. 표준 오류
PDCOMM_LOG_MAX_BYTES 회전 크기(8192바이트~1GiB) 10MiB
PDCOMM_LOG_FILES 보관 파일 수(현재 파일 포함, 1~16) 3
PDCOMM_LOG_FORENSICS 주기적인 프로세스 표본 추가. debug 이상에서만 꺼짐
  • 환경 변수는 라이브러리가 로거를 처음 쓸 때 한 번 읽습니다. 바꾼 뒤에는 프로그램을 다시 시작합니다. GUI도 이 환경에서 시작해야 적용됩니다.
  • 헤드리스와 MCP 서버의 표준 출력에는 로그가 섞이지 않습니다. 파일을 지정하지 않으면 표준 오류로 갑니다.
  • 클라이언트와 서버처럼 프로세스가 둘이면 서로 다른 파일을 지정합니다.
  • 로그에는 PDU 내용, 키, 인증 정보를 기록하지 않습니다.

오류 수준의 기록만 봅니다.

터미널 창
jq -c 'select(.level=="error") | {component, event, fields}' logs/client.jsonl
{"component":"tcp_client","event":"failed","fields":{"connection_id":3,"error_kind":null,"error_type":"Timeout","max_poll_gap_us":2692,"pending":[{"deadline_exceeded":true,"elapsed_us":1000930,"function":3,"max_poll_gap_us":2692,"request_id":4,"transaction_id":0,"unit":1}],"protocol_error":null,"queued":0,"rx_bytes":0,"rx_partial_bytes":0,"timeout":true,"tx_bytes":12,"tx_offset":0}}
{"component":"service","event":"action_completed","fields":{"action":"request","elapsed_us":1001041,"error_code":"Timeout","success":false,"transport":"tcp"}}
{"component":"ffi","event":"call_completed","fields":{"elapsed_us":1001107,"endpoint_id":1,"success":false}}

failed 기록이 원인을 말해 줍니다.

  • tx_bytes 12: 요청 ADU(MBAP 7바이트 + PDU 5바이트)는 모두 보냈습니다.
  • rx_bytes 0, rx_partial_bytes 0: 응답은 한 바이트도 오지 않았습니다. 상대가 처리하지 않았거나 응답을 보내지 않은 것입니다.
  • elapsed_us 약 1000000, deadline_exceeded true: timeout_ms 1000을 다 기다렸습니다.
  • max_poll_gap_us가 작습니다: SDK가 늦게 처리해서 생긴 시간 초과가 아닙니다.

응답 일부만 왔다면 rx_partial_bytes가 0보다 큽니다. 이때는 상대의 응답이 중간에 끊긴 것이므로 상대 장비나 네트워크를 봅니다.

같은 요청의 기록 전체는 request_id로 모읍니다. 번호는 실행마다 다릅니다.

터미널 창
jq -c 'select(.request_id==4) | [.component, .event]' logs/client.jsonl
["ffi","call_entered"]
["ffi","lock_acquired"]
["service","action_started"]
["tcp_client","submitted"]
["tcp_client","socket_write"]
["tcp_client","failed"]
["service","action_completed"]
["ffi","call_completed"]

정상 요청이라면 socket_write 뒤에 socket_read와 completed가 이어집니다. transaction_id는 연결마다 다시 0부터 시작하므로, 기록을 맞출 때는 connection_id와 함께 봅니다.

6. 실행 중에 로그를 제어합니다

섹션 제목: “6. 실행 중에 로그를 제어합니다”

환경 변수 없이 프로그램 안에서 켜고 끌 수도 있습니다. 설정 형식은 환경 변수와 같은 항목의 JSON입니다(enabled, level, file, max_bytes, files, forensics).

DiagnosticLogging을 씁니다.

examples/diagnostics/modbus/logging.cpp
#include <pdcomm/modbus/pdcomm_modbus.hpp>
#include <pdcomm/modbus/pdcomm_modbus_logging.hpp>
#include <iostream>
int main(int argc, char **argv) {
if (argc != 2) return 2;
try {
pdcomm::modbus::DiagnosticLogging::configure(argv[1]);
pdcomm::modbus::Endpoint endpoint;
std::cout << endpoint.call(R"({"action":"execute","pdu":[6,0,0,0,42]})") << '\n';
pdcomm::modbus::DiagnosticLogging::flush();
std::cout << pdcomm::modbus::DiagnosticLogging::status() << '\n';
pdcomm::modbus::DiagnosticLogging::configure(R"({"enabled":false})");
std::cout << pdcomm::modbus::DiagnosticLogging::status() << '\n';
} catch (const std::exception &error) {
std::cerr << error.what() << '\n';
return 1;
}
return 0;
}
터미널 창
c++ -std=c++17 -I "$MODBUS_SDK/include" "$MODBUS_EXAMPLES/diagnostics/modbus/logging.cpp" -L "$MODBUS_SDK/lib" -lpdcomm_modbus_ffi -Wl,-rpath,"$MODBUS_SDK/lib" -o logging-example
./logging-example '{"level":"debug","file":"logs/logging-cpp.jsonl"}'

두 예제 모두 세 줄을 출력합니다. 명령 결과, 로그를 켠 상태("enabled":true, written 증가), 끈 상태("enabled":false)입니다.

헤드리스와 MCP는 같은 제어를 명령으로 제공합니다(logging_configure, logging_status, logging_flush, MCP는 modbus_ 접두사).

{"action":"logging_configure","config":{"enabled":true,"level":"debug","file":"logs/headless.jsonl"}}
{"action":"logging_configure","config":{"enabled":false}}

잘못된 설정(예: "level":"verbose")은 Invalid logging level로 실패하고, 기존 설정을 그대로 둔 채 configuration_errors를 1 늘립니다. C API는 pdcomm_modbus_logging_configure입니다. 설정과 플러시는 최대 2초를 기다릴 수 있으므로 통신 루프 밖에서 호출합니다.

수집 도구로 로그를 모으려면 회전 파일(*.jsonl, .1, .2)을 함께 읽게 설정합니다. 다음은 Fluent Bit 예입니다. 경로와 버전은 환경에 맞게 바꿉니다.

examples/diagnostics/collector/fluent-bit.conf
[SERVICE]
Flush 1
Parsers_File parsers.conf
[INPUT]
Name tail
Path /var/log/pdcomm/*.jsonl*
Tag pdcomm
Parser pdcomm_json
DB /var/lib/fluent-bit/pdcomm-offsets.db
DB.Sync Full
DB.Compare_Filename Off
Refresh_Interval 1
Rotate_Wait 30
Mem_Buf_Limit 10MB
Buffer_Chunk_Size 16k
Buffer_Max_Size 16k
Skip_Long_Lines Off
Read_From_Head On
[OUTPUT]
Name stdout
Match pdcomm
Format json_lines
Json_date_key ingested_at
Json_date_format double

같은 세션에서 다음 명령의 결과를 저장합니다.

{"action":"state"}
{"action":"diagnostics"}
{"action":"trace"}
{"action":"project_export"}
  • trace는 최근 1000개만 보관합니다. 재현 전에 trace_clear하고, 실패 직후 저장합니다.
  • project_export는 result만 파일로 저장합니다. 그 파일을 그대로 project_import에 쓸 수 있습니다.
  • 직렬 trace의 시각은 PC의 입출력 묶음 시각이므로 UART 비트 시각으로 보고하지 않습니다.
  • 표준 출력과 표준 오류를 따로 저장하고 종료 코드도 남깁니다.

실제 값으로 채워 담당자에게 보냅니다. 이 과정은 외부로 자동 전송하지 않습니다.

SDK 버전 / manifest.json의 capabilities, abi:
OS 버전 / CPU / 컴파일러 / .NET SDK와 런타임:
전송 방식 / 클라이언트·서버 역할 / unit / 주소 또는 직렬 설정:
준비 과정과 정확한 명령 또는 GUI 조작:
기대한 응답과 값:
실제 response / status / source / 오류 메시지 / 종료 코드:
클라이언트와 서버 trace, 민감 정보를 지운 프로젝트, 화면 캡처:
진단 로그(JSONL)와 logging_status의 dropped, write_errors:
재현 빈도 / 처음 실패한 단계 / 복구 방법:
장비 모델과 어댑터, 배선(직렬 장비만):

개인 키, API 키, 고객 데이터 원문은 넣지 않습니다. 인증서는 경로, 공개 인증서의 지문(fingerprint)과 유효 기간만 적습니다.

터미널 A의 상대는 30초 뒤 끝납니다. 기다리지 않으려면 Ctrl+C로 끝냅니다. logs 폴더는 문의에 쓴 뒤 지웁니다.