Diagnose failures and report them
When communication fails, find the layer that failed first, reduce the reproduction, then collect evidence under the same conditions. In this tutorial you build a TCP peer that never answers, reproduce a timeout and confirm its cause from the diagnostic log.
- Narrow a failure down to one layer: SDK loading, configuration, connection, transaction or device meaning.
- Turn on the JSON Lines diagnostic log with
PDCOMM_LOGand read the record of a timeout. - Turn logging on and off at run time through the API and commands.
- Collect what a support request needs.
What you need
Section titled “What you need”- The SDK, examples and C++ console from the quickstart
- Python 3 and
jq - Two terminals
1. Find the failing layer
Section titled “1. Find the failing layer”Check the layers in this order; once one fails, the later ones don’t matter yet.
| Layer | Check | Failures at this layer |
|---|---|---|
| SDK loading | Does {"action":"capabilities"} answer? |
DllNotFoundException, library not found |
| Configuration | Is the configure result ok? |
Invalid endpoint configuration, missing field `port` |
| Connection | Does connect report connected as true? |
Io(ConnectionRefused, ...), Io(AddrInUse, ...) |
| Transaction | Does request return a result? |
Timeout, Closed, Protocol(...) |
| Device meaning | Is status success, and does the value match the manual? |
Exceptions 01, 02, 03; a value from the neighboring address |
okset totruecan still carry a Modbus exception; checkstatustoo.- A result with
sourcelocalisn’t communication. Judge communication with a device bywireresults. no_responsefor broadcasts and in listen-only mode is intentional.
2. Reduce it to a minimal reproduction
Section titled “2. Reduce it to a minimal reproduction”- In a new process, record
capabilitiesandstate. - If you can, check functions 06 and 03 first on
127.0.0.1on the same computer, without sharing ports with other equipment. - Keep only the one failing request, and record its exact PDU,
unit,response,statusandsource. - Read the client’s and the server’s
traceand line them up. - Record the processes’ exit codes.
If a write timed out, the peer may already have applied it. Read the value before writing again: PDComm never resends writes automatically.
3. Create a peer that never answers
Section titled “3. Create a peer that never answers”In terminal A, run a TCP peer that accepts a connection but never sends anything. It ends by itself after 30 seconds.
python3 -c "import socket,time; s=socket.create_server(('127.0.0.1',1503)); c,_=s.accept(); time.sleep(30)"4. Turn on the log and reproduce
Section titled “4. Turn on the log and reproduce”In terminal B, enable the log with environment variables and start the C++ console:
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"}Finally, enter quit to end the console. The request fails after one second with Command error: {"error":"Timeout","ok":false}. In logging_status, written is the number of records written; dropped, write_errors and configuration_errors at 0 mean no records were lost.
| Variable | Meaning | Default |
|---|---|---|
PDCOMM_LOG |
error, warn, info, debug, trace or off |
Off |
PDCOMM_LOG_FILE |
Log file path; without it, records go to standard error. | Standard error |
PDCOMM_LOG_MAX_BYTES |
Rotation size (8192 bytes to 1 GiB) | 10 MiB |
PDCOMM_LOG_FILES |
Files kept, including the current one (1–16) | 3 |
PDCOMM_LOG_FORENSICS |
Adds periodic process samples; debug or trace only |
Off |
- The library reads these variables once, when it first uses its logger. Restart the program after changing them. Start the GUI from such an environment too.
- The headless simulator and the MCP server never mix log records into standard output; without a file they log to standard error.
- When you run two processes, such as a client and a server, give each its own file.
- The log never contains PDU contents, keys or credentials.
5. Read the log
Section titled “5. Read the log”Show only the error-level records:
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}}The failed record explains the cause:
tx_bytes12: the whole request ADU (7-byte MBAP header plus 5-byte PDU) was sent.rx_bytes0 andrx_partial_bytes0: not a single response byte arrived. The peer didn’t process the request or didn’t answer.elapsed_usof about 1000000 anddeadline_exceededtrue: the client waited the fulltimeout_msof 1000.- A small
max_poll_gap_us: the timeout wasn’t caused by the SDK processing late.
If part of a response had arrived, rx_partial_bytes would be above 0, meaning the peer’s response was cut off; look at the peer device or the network.
Collect every record of one request by its request_id. The number differs between runs.
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"]A successful request continues with socket_read and completed after socket_write. transaction_id starts again at 0 on every connection, so match records by connection_id as well.
6. Control the log at run time
Section titled “6. Control the log at run time”You can also turn logging on and off from inside a program. The configuration is JSON with the same items as the variables (enabled, level, file, max_bytes, files, forensics).
Use 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"}'Use 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"Both examples print three lines: the command result, the status with logging on ("enabled":true, written increased) and the status with logging off ("enabled":false).
The headless simulator and the MCP server offer the same control as commands (logging_configure, logging_status, logging_flush; MCP adds the modbus_ prefix):
{"action":"logging_configure","config":{"enabled":true,"level":"debug","file":"logs/headless.jsonl"}}{"action":"logging_configure","config":{"enabled":false}}An invalid configuration such as "level":"verbose" fails with Invalid logging level, keeps the previous configuration and increments configuration_errors. In C, use pdcomm_modbus_logging_configure. Configuring and flushing can wait up to two seconds, so call them outside your communication loop.
To ship the log with a collector, make it read the rotated files (*.jsonl, .1, .2) as well. Here’s a Fluent Bit example; adjust the paths and version for your environment:
[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. Collect the evidence
Section titled “7. Collect the evidence”In the same session, save the results of:
{"action":"state"}{"action":"diagnostics"}{"action":"trace"}{"action":"project_export"}tracekeeps only the last 1000 entries. Runtrace_clearbefore reproducing and save right after the failure.- Save only the
resultofproject_export; that file works as is withproject_import. - Serial
tracetimes are PC I/O batch times; don’t report them as UART bit times. - Save standard output and standard error separately, and keep the exit codes.
8. Fill in the support request
Section titled “8. Fill in the support request”Fill this in with real values and send it to your contact. Nothing in this process sends data anywhere automatically.
SDK version / capabilities and abi from manifest.json:OS version / CPU / compiler / .NET SDK and runtime:Transport / client and server roles / unit / address or serial settings:Preparation and the exact commands or GUI actions:Expected response and values:Actual response / status / source / error message / exit code:Client and server trace, project with sensitive data removed, screenshots:Diagnostic log (JSONL) and dropped, write_errors from logging_status:How often it happens / first failing step / recovery:Device model, adapter and wiring (serial devices only):Never include private keys, API keys or raw customer data. For certificates, give paths, the public certificate fingerprint and the validity period only.
Clean up
Section titled “Clean up”The peer in terminal A ends after 30 seconds; press Ctrl+C to end it sooner. Delete the logs folder once you’ve used it in your request.