Skip to content
Start free evaluation

Capture and read a diagnostic log

In this tutorial you turn on the Modbus SDK diagnostic log in two ways. First, an example program turns logging on and off through the API. Then you turn on logging for the headless simulator with environment variables and trace a request to a device that never answers.

  • Turn on logging through the API and check the written and dropped counts.
  • Group the records of one request by request_id and read its processing steps.
  • Find the device and function code of a failed request in the timeout record.
  • macOS or Linux, a C++14 compiler for the C++ example, and the .NET 8 SDK for the C# example.
  • The Modbus SDK (include/, lib/, bin/, bindings/). The commands below find the SDK folder through MODBUS_SDK and the examples folder through EXAMPLES.
  • For step 4, the lab and server from Test Modbus RTU on a virtual RS-485 bus.
터미널 창
export MODBUS_SDK="$HOME/PDComm/pdcomm-modbus" # replace with your SDK folder
export EXAMPLES="$HOME/PDComm/examples" # replace with your examples folder
mkdir -p "$HOME/PDComm/logging-lab/logs" && cd "$HOME/PDComm/logging-lab"

The example turns on logging with the configuration from its command-line argument, runs one write request against the local memory model, prints the status, and turns logging off.

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;
}
  1. Build the C++ example. -Wl,-rpath lets the program find the library in the SDK’s lib/ folder.

    터미널 창
    c++ -std=c++14 -I "$MODBUS_SDK/include" "$EXAMPLES/diagnostics/modbus/logging.cpp" \
    -L "$MODBUS_SDK/lib" -lpdcomm_modbus_ffi -Wl,-rpath,"$MODBUS_SDK/lib" -o logging-example
  2. Run it. The configuration JSON asks for debug records in ./logs/modbus-cpp.jsonl.

    터미널 창
    ./logging-example '{"level":"debug","file":"./logs/modbus-cpp.jsonl"}'

    The program prints three lines: the reply to the write request, the status with logging on, and the status after it turned logging off. In the second line, check "enabled":true, "written":5, "dropped":0, and "write_errors":0.

    {"ok":true,"result":{"response":[6,0,0,0,42],"source":"local","status":"success"}}
    {"build_id":"…","configuration_errors":0,"dropped":0,"enabled":true,…,"sink":"file","write_errors":0,"written":5}
    {"build_id":"…","configuration_errors":0,"dropped":0,"enabled":false,…,"level_name":"off",…,"written":5}
  1. The log file holds five records, one JSON object per line. Print the main fields:

    터미널 창
    python3 -c 'import json,sys; [print(r["sequence"], r["level"], r["component"], r["event"], r["request_id"]) for r in map(json.loads, open(sys.argv[1]))]' logs/modbus-cpp.jsonl
    1 debug ffi call_entered 1
    2 debug ffi lock_acquired 1
    3 debug service action_started 1
    4 debug service action_completed 1
    5 debug ffi call_completed 1
  2. All five records share request_id 1. One API call was logged as it entered the C ABI (call_entered), took the endpoint lock (lock_acquired), ran in the service (action_started, action_completed), and returned (call_completed). wait_us and elapsed_us in fields show the lock wait and the processing time.

The C# example builds the Modbus .NET binding source as a project reference, without any external NuGet package.

diagnostics/modbus/Program.cs
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;
  1. Build it, passing the SDK folder in the PdcommModbusSdk property:

    터미널 창
    dotnet build "$EXAMPLES/diagnostics/modbus/LoggingExample.csproj" -c Release \
    -p:PdcommModbusSdk="$MODBUS_SDK" -o logging-dotnet
  2. Run it. The first argument is the absolute path of the native library; on Linux, pass libpdcomm_modbus_ffi.so.

    터미널 창
    dotnet logging-dotnet/LoggingExample.dll "$MODBUS_SDK/lib/libpdcomm_modbus_ffi.dylib" "$PWD/logs/modbus-csharp.jsonl"

    You get the same three lines as from the C++ example, and five records in logs/modbus-csharp.jsonl.

Step 4: Turn on headless simulator logging with environment variables

Section titled “Step 4: Turn on headless simulator logging with environment variables”

To turn on logging without changing code, use environment variables. This time you send a request to a device that doesn’t answer, so the log records a failure.

  1. In terminal 1, go to the vserial/ examples folder from the Modbus RTU tutorial and start the bus:

    터미널 창
    cd "$EXAMPLES/vserial" && pdcomm-vserial run --config rs485-bus.json
  2. In terminal 2, start the unit 1 server:

    터미널 창
    cd "$EXAMPLES/vserial" && cat modbus-rtu/drive1.jsonl - | "$MODBUS_SDK/bin/pdcomm-modbus-headless"
  3. In terminal 3, start a client with logging on. The log file goes to logging-lab/logs.

    터미널 창
    cd "$EXAMPLES/vserial"
    cat modbus-rtu/master.jsonl - | PDCOMM_LOG=debug \
    PDCOMM_LOG_FILE="$HOME/PDComm/logging-lab/logs/rtu-client.jsonl" "$MODBUS_SDK/bin/pdcomm-modbus-headless"
  4. Type two requests into terminal 3. Unit 1 answers; unit 9 isn’t on the bus.

    {"action":"request","unit":1,"pdu":"0300000001"}
    {"action":"request","unit":9,"pdu":"0300000001"}

    The first request returns [3,2,5,220], and the second ends after one second with {"error":"Timeout","ok":false}.

  5. Clean up the three terminals: press Ctrl+D in terminals 3 and 2, and type {"command":"stop"} in terminal 1.

  1. From the logging-lab folder, print the records, this time with their fields:

    터미널 창
    cd "$HOME/PDComm/logging-lab"
    python3 -c 'import json,sys; [print(r["sequence"], r["level"], r["component"], r["event"], r["request_id"], json.dumps(r["fields"])) for r in map(json.loads, open(sys.argv[1]))]' logs/rtu-client.jsonl
  2. The records of the last request look like this (the request_id and times differ from run to run):

    9 debug service action_started 7 {"action": "request", "role": "client", "timeout_ms": 1000, "transport": "rtu", "unit": 9}
    10 debug serial io_batch 7 {"baud": 19200, "bytes": 8, "direction": "tx", "timestamp_us": 19916, "transport": "Rtu"}
    11 error serial timeout 7 {"function": 3, "received_bytes": 0, "unit": 9}
    12 error service action_completed 7 {"action": "request", "elapsed_us": 1003520, "error_code": "Timeout", "success": false, "transport": "rtu"}
  3. Interpret them:

    • io_batch with "direction": "tx" and "bytes": 8: the 8-byte request frame was written to the port, so the request left for the line.
    • timeout with "received_bytes": 0: not a single reply byte arrived, which rules out a partial reply.
    • elapsed_us in action_completed: the client waited for the full configured timeout (1000 ms).

    So the SDK sent the request and unit 9 never answered. In a real system you would now check the device address, the wiring, and the device’s power.

  • Delete the logging-lab folder. The logs contain no payloads, but they do contain peer addresses and port names, so share only the parts you need.
  • Make sure PDCOMM_LOG isn’t set in your shell. This tutorial set the variables for single commands only.
Symptom Cause and fix
No log file appears One invalid value makes the library ignore the whole environment configuration. PDCOMM_LOG must be a lowercase level name. Check configuration_errors in the status.
The exception Cannot configure PDComm logging The configuration JSON is invalid or the file can’t be opened. Check that the folder exists.
Expected records are missing The program exited without flushing, or records were dropped. Check dropped and write_errors in the status.
The C# example exits with code 2 It didn’t get two arguments, or the native library path isn’t absolute.