실패 진단과 문의
통신이 실패하면 먼저 어느 층에서 실패했는지 찾고, 재현 조건을 줄인 뒤, 같은 조건에서 증거를 모읍니다. 이 튜토리얼은 응답하지 않는 TCP 상대를 만들어 시간 초과를 재현하고, 진단 로그로 원인을 확인합니다.
- 실패를 SDK 로딩, 구성, 연결, 거래, 장비 의미 가운데 한 층으로 좁힙니다.
PDCOMM_LOG환경 변수로 JSON Lines 진단 로그를 켜고, 시간 초과 기록을 읽습니다.- 실행 중에 로그를 켜고 끄는 API와 명령을 씁니다.
- 문의에 필요한 정보를 모읍니다.
준비물
섹션 제목: “준비물”- 빠른 시작의 SDK, 예제, C++ 콘솔
- Python 3,
jq - 터미널 두 개
1. 실패한 층을 찾습니다
섹션 제목: “1. 실패한 층을 찾습니다”아래 순서로 확인합니다. 앞 층이 실패하면 뒤 층은 볼 필요가 없습니다.
| 층 | 확인 방법 | 이 층의 실패 예 |
|---|---|---|
| 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는 의도된 무응답입니다.
2. 최소 재현으로 줄입니다
섹션 제목: “2. 최소 재현으로 줄입니다”- 새 프로세스에서
capabilities와state를 기록합니다. - 가능하면 같은 PC의
127.0.0.1에서 기능 06과 03으로 먼저 확인합니다. 다른 장비와 포트를 공유하지 않습니다. - 실패하는 요청 하나만 남기고, 정확한 PDU,
unit,response,status,source를 기록합니다. - 클라이언트와 서버의
trace를 각각 읽어 순서를 맞춥니다. - 프로세스의 종료 코드를 기록합니다.
쓰기 요청이 시간 초과되면 상대가 이미 처리했을 수 있습니다. 다시 쓰기 전에 값을 읽어 확인합니다. 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 logsPDCOMM_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 내용, 키, 인증 정보를 기록하지 않습니다.
5. 로그를 읽습니다
섹션 제목: “5. 로그를 읽습니다”오류 수준의 기록만 봅니다.
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_bytes12: 요청 ADU(MBAP 7바이트 + PDU 5바이트)는 모두 보냈습니다.rx_bytes0,rx_partial_bytes0: 응답은 한 바이트도 오지 않았습니다. 상대가 처리하지 않았거나 응답을 보내지 않은 것입니다.elapsed_us약 1000000,deadline_exceededtrue:timeout_ms1000을 다 기다렸습니다.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을 씁니다.
#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"}'DiagnosticLogging을 씁니다.
using System.Runtime.InteropServices;if (args.Length != 2 || !Path.IsPathFullyQualified(args[0])) return 2;NativeLibrary.SetDllImportResolver(typeof(PDComm.Modbus.Endpoint).Assembly, (name, assembly, search) => name == "pdcomm_modbus_ffi" ? NativeLibrary.Load(args[0]) : IntPtr.Zero);PDComm.Modbus.DiagnosticLogging.Configure(new { enabled = true, level = "debug", file = args[1] });using (var endpoint = new PDComm.Modbus.Endpoint()) Console.WriteLine(endpoint.Call(new { action = "execute", pdu = new[] { 6, 0, 0, 0, 42 } }));PDComm.Modbus.DiagnosticLogging.Flush();Console.WriteLine(PDComm.Modbus.DiagnosticLogging.Status());PDComm.Modbus.DiagnosticLogging.Configure(new { enabled = false });Console.WriteLine(PDComm.Modbus.DiagnosticLogging.Status());return 0;dotnet build "$MODBUS_EXAMPLES/diagnostics/modbus/LoggingExample.csproj" -c Release -p:PdcommModbusSdk="$MODBUS_SDK" -o build/logging-dotnetdotnet build/logging-dotnet/LoggingExample.dll "$MODBUS_SDK/lib/libpdcomm_modbus_ffi.dylib" "$PWD/logs/logging-csharp.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 예입니다. 경로와 버전은 환경에 맞게 바꿉니다.
[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 double7. 증거를 모읍니다
섹션 제목: “7. 증거를 모읍니다”같은 세션에서 다음 명령의 결과를 저장합니다.
{"action":"state"}{"action":"diagnostics"}{"action":"trace"}{"action":"project_export"}trace는 최근 1000개만 보관합니다. 재현 전에trace_clear하고, 실패 직후 저장합니다.project_export는result만 파일로 저장합니다. 그 파일을 그대로project_import에 쓸 수 있습니다.- 직렬
trace의 시각은 PC의 입출력 묶음 시각이므로 UART 비트 시각으로 보고하지 않습니다. - 표준 출력과 표준 오류를 따로 저장하고 종료 코드도 남깁니다.
8. 문의 양식을 채웁니다
섹션 제목: “8. 문의 양식을 채웁니다”실제 값으로 채워 담당자에게 보냅니다. 이 과정은 외부로 자동 전송하지 않습니다.
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 폴더는 문의에 쓴 뒤 지웁니다.