diff --git a/.bazelrc b/.bazelrc
index 247b2162..83cbdc4f 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -57,6 +57,7 @@ build:time-arm64-linux --extra_toolchains=@score_gcc_aarch64_toolchain//:aarch64
build:time-x86_64-qnx --config=time_shared
build:time-x86_64-qnx --platforms=@score_bazel_platforms//:x86_64-qnx-sdp_8.0.0-posix
build:time-x86_64-qnx --extra_toolchains=@score_qcc_x86_64_toolchain//:x86_64-qnx-sdp_8.0.0
+build:time-x86_64-qnx --extra_toolchains=@score_qnx_x86_64_ifs_toolchain//:ifs-x86_64-qnx-sdp_8.0.0
# -------------------------------------------------------------------------------
# Config dedicated to target platform CPU:arm64 and OS:QNX
diff --git a/MODULE.bazel b/MODULE.bazel
index 4a01c4bc..9696559c 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -57,6 +57,7 @@ use_repo(
"score_gcc_x86_64_toolchain",
"score_qcc_aarch64_toolchain",
"score_qcc_x86_64_toolchain",
+ "score_qcc_x86_64_toolchain_pkg",
)
## Custom Module Loading
@@ -212,3 +213,55 @@ pip.parse(
requirements_lock = "//third_party/codeql:requirements_lock.txt",
)
use_repo(pip, "codeql_coding_standards_pip_hub")
+## Integration Test Framework dependencies
+
+bazel_dep(name = "score_itf", version = "0.2.0", dev_dependency = True)
+git_override(
+ module_name = "score_itf",
+ commit = "98118b1fcd6c53244fb276e2c7668faaf15d2e10", # tag v0.4.0
+ remote = "https://github.com/eclipse-score/itf.git",
+)
+
+bazel_dep(name = "bazel_skylib", version = "1.9.0", dev_dependency = True)
+bazel_dep(name = "rules_pkg", version = "1.2.0", dev_dependency = True)
+bazel_dep(name = "rules_oci", version = "2.2.7", dev_dependency = True)
+
+oci = use_extension("@rules_oci//oci:extensions.bzl", "oci", dev_dependency = True)
+oci.pull(
+ name = "ubuntu_24_04",
+ digest = "sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30",
+ image = "ubuntu",
+ platforms = ["linux/amd64"],
+ tag = "24.04",
+)
+use_repo(oci, "ubuntu_24_04", "ubuntu_24_04_linux_amd64")
+
+bazel_dep(name = "score_rules_imagefs", version = "0.0.3", dev_dependency = True)
+
+imagefs = use_extension("@score_rules_imagefs//extensions:imagefs.bzl", "imagefs", dev_dependency = True)
+imagefs.toolchain(
+ name = "score_qnx_x86_64_ifs_toolchain",
+ sdp_to_import = "@score_qcc_x86_64_toolchain_pkg",
+ # imagefs extension does not remap 8.0.4 -> 8.0.0 for platform constraints,
+ # so use "8.0.0" here to match the only available sdp_version constraint
+ # value in @score_bazel_platforms. The actual SDP binary version comes
+ # from sdp_to_import (8.0.4).
+ sdp_version = "8.0.0",
+ target_cpu = "x86_64",
+ target_os = "qnx",
+ type = "ifs",
+)
+use_repo(imagefs, "score_qnx_x86_64_ifs_toolchain")
+
+## JSON Schema validation (build-time lint)
+
+bazel_dep(name = "nlohmann_json", version = "3.11.3", dev_dependency = True)
+
+http_archive(
+ name = "json_schema_validator",
+ build_file = "//third_party/json_schema_validator:json_schema_validator.BUILD",
+ dev_dependency = True,
+ integrity = "sha256-g/YdgRL0heDT8ectUWELo5JLF5kmqDdq7zwDh3D68gI=",
+ strip_prefix = "json-schema-validator-2.1.0",
+ urls = ["https://github.com/pboettch/json-schema-validator/archive/refs/tags/2.1.0.tar.gz"],
+)
diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock
index 96859963..f2b9a622 100644
--- a/MODULE.bazel.lock
+++ b/MODULE.bazel.lock
@@ -39,6 +39,7 @@
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.21.1/MODULE.bazel": "07e3ce3eaaa50dbd0be7fa0094e36890478937adc780ec53e77fd9fe543af8b1",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.0/MODULE.bazel": "7fe0191f047d4fe4a4a46c1107e2350cbb58a8fc2e10913aa4322d3190dec0bf",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.0/source.json": "369df5b7f2eae82f200fff95cf1425f90dee90a0d0948122060b48150ff0e224",
+ "https://bcr.bazel.build/modules/aspect_bazel_lib/2.7.2/MODULE.bazel": "780d1a6522b28f5edb7ea09630748720721dfe27690d65a2d33aa7509de77e07",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.7.7/MODULE.bazel": "491f8681205e31bb57892d67442ce448cda4f472a8e6b3dc062865e29a64f89c",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.8.1/MODULE.bazel": "812d2dd42f65dca362152101fbec418029cc8fd34cbad1a2fde905383d705838",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.9.3/MODULE.bazel": "66baf724dbae7aff4787bf2245cc188d50cb08e07789769730151c0943587c14",
@@ -478,6 +479,7 @@
"https://bcr.bazel.build/modules/rules_nodejs/6.5.2/MODULE.bazel": "7f9ea68a0ce6d82905ce9f74e76ab8a8b4531ed4c747018c9d76424ad0b3370d",
"https://bcr.bazel.build/modules/rules_nodejs/6.7.3/MODULE.bazel": "c22a48b2a0dbf05a9dc5f83837bbc24c226c1f6e618de3c3a610044c9f336056",
"https://bcr.bazel.build/modules/rules_nodejs/6.7.3/source.json": "a3f966f4415a8a6545e560ee5449eac95cc633f96429d08e87c87775c72f5e09",
+ "https://bcr.bazel.build/modules/rules_oci/2.2.7/MODULE.bazel": "f6150e4b224d459f7f6523ef65967464ca4efdd266c7fbf2f5a2a51011957e0c",
"https://bcr.bazel.build/modules/rules_oci/2.3.0/MODULE.bazel": "49075197960c924c0a4d759b7c765c3d00a41d2fdd4a943b42823c1d016ab4ec",
"https://bcr.bazel.build/modules/rules_oci/2.3.0/source.json": "47710c28446211b5e61a24015a4669c50c6862d5f91e6bdbc710de8d750cf613",
"https://bcr.bazel.build/modules/rules_perl/1.1.0/MODULE.bazel": "22138e75bb8f1ee6c21f609b90d2c24b0c9b796ccf55cc04c1c9190b699f7e9d",
@@ -611,6 +613,7 @@
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_bazel_lib/2.20.0/MODULE.bazel": "not found",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_bazel_lib/2.21.1/MODULE.bazel": "not found",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_bazel_lib/2.22.0/MODULE.bazel": "not found",
+ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_bazel_lib/2.7.2/MODULE.bazel": "not found",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_bazel_lib/2.7.7/MODULE.bazel": "not found",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_bazel_lib/2.8.1/MODULE.bazel": "not found",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_bazel_lib/2.9.3/MODULE.bazel": "not found",
@@ -942,6 +945,7 @@
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_nodejs/6.3.3/MODULE.bazel": "not found",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_nodejs/6.5.2/MODULE.bazel": "not found",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_nodejs/6.7.3/MODULE.bazel": "not found",
+ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_oci/2.2.7/MODULE.bazel": "not found",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_oci/2.3.0/MODULE.bazel": "not found",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_perl/1.1.0/MODULE.bazel": "not found",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_pkg/0.7.0/MODULE.bazel": "not found",
@@ -1035,6 +1039,7 @@
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_docs_as_code/3.0.1/MODULE.bazel": "296328e60d5481ff529e0c937ba80c5d3d4838445d119caf517fd82f97cd053a",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_docs_as_code/4.0.0/MODULE.bazel": "522dc070354e6be2f984468a4243fe4ab8bec690922df5f31f7f0916ee264e20",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_docs_as_code/4.0.1/MODULE.bazel": "5955f4cf37228a9cdda7f6009b81db0446f005c618f4bc43665bfa45f2673ebc",
+ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_docs_as_code/4.0.3/MODULE.bazel": "ad24df8df93882297e36ecaa39a94a69eed68aeb5bb1614e85296d001d5e3c03",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_docs_as_code/4.5.0/MODULE.bazel": "4cfe52fe8b8dbeaf7e87500036391da278f72f1c2b41b689ffdd4337196dd8fe",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_docs_as_code/4.5.0/source.json": "e01b29a3e9640a0d41d880d7da525e451115649d90f097ed66d09808c9135486",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_format_checker/0.1.1/MODULE.bazel": "1acc254faa90e9f97b79ac69af25b6c21c561f8d6079914f6352b9b20d26bd37",
@@ -1056,12 +1061,15 @@
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_process/1.4.3/MODULE.bazel": "eb8243299a6ae3663618db5b4718e2c0d3c93f91a44994641e70106c400b23fb",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_process/1.5.2/MODULE.bazel": "be52d29278d6671221f28921e8f1acfce29c3bfc3e6b4f503f0625ab2c61586f",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_process/1.5.3/MODULE.bazel": "65024b7f23ce5f72bd6ffd455a67c042ecf56d267f0bf63a90330a3241781b7a",
+ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_process/1.5.4/MODULE.bazel": "efd56704f1a93e670032e7c0e4fad97669aa3b348fb1a4cd40f67e4dd4c7037c",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_process/1.6.0/MODULE.bazel": "2496bc24311f69f49449ee85d8bb38e3b970cbfcf10d0a7f19b2d5262ce80e8d",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_process/1.6.0/source.json": "093424aa8bfed8705a3d142b21fe1d053258f2dd5eb1944941f6439b2c7157e9",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_python_basics/0.3.0/MODULE.bazel": "785ddd5295213e36c31ab86bdc34f29c0f7d1b72e9abd931bb08f42c0e48e2e9",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_python_basics/0.3.1/MODULE.bazel": "99c491109937542e61df090222666a8613ef946fa7bb2b2d5ba648b2baba03ad",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_python_basics/0.3.2/MODULE.bazel": "f25490f64035a0e3a0d53ad9cb6164e8325ce6cf2a7ee68c6ae153840cb2497e",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_python_basics/0.3.4/MODULE.bazel": "53bd16dfbb1fb8ecf6822fb26f9f4e8333bac7b14d12bb02bf84078063820a31",
+ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_rules_imagefs/0.0.3/MODULE.bazel": "d87fcd3485438284fa96d6a5a81923d6b6672468947423517cc43c4d7692ca6a",
+ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_rules_imagefs/0.0.3/source.json": "5f2de07264fbc8f3adecffde5aea326b9f8fccf9e74fd6a057fc9c3556b00cfa",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_rust_policies/0.0.2/MODULE.bazel": "ade2bad4a331b02d9b7e7d9842e8de8c6fded6186486e02c4f7db5cd4b71d34d",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_rust_policies/0.0.3/MODULE.bazel": "9e8310a75c13ccebc49fb9cbf7acc6c1b75654292b2ca907fb5d513133dbf6f3",
"https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_rust_policies/0.0.5/MODULE.bazel": "7de02547bdf121d3dedf5141b97f0fd9a545bd255ff5c7b699056b35816ffad9",
@@ -9912,6 +9920,44 @@
]
]
}
+ },
+ "@@score_rules_imagefs+//extensions:imagefs.bzl%imagefs": {
+ "general": {
+ "bzlTransitiveDigest": "6ZLANe53LXwcnne1ZrZm9GkJ9H5VdsCLSgSNHyUrqA4=",
+ "usagesDigest": "1ieOQTsTmqRePgmksL8DDGNxZZT2g0ExwBiH6NLXBLg=",
+ "recordedFileInputs": {},
+ "recordedDirentsInputs": {},
+ "envVariables": {},
+ "generatedRepoSpecs": {
+ "score_qnx_x86_64_ifs_toolchain": {
+ "repoRuleId": "@@score_rules_imagefs+//rules/qnx:imagefs_toolchain.bzl%imagefs_toolchain",
+ "attributes": {
+ "tc_cpu": "x86_64",
+ "tc_os": "qnx",
+ "tc_pkg_repo": "'@@score_bazel_cpp_toolchains++gcc+score_qcc_x86_64_toolchain_pkg'",
+ "sdp_version": "8.0.0",
+ "tc_type": "ifs"
+ }
+ }
+ },
+ "recordedRepoMappingEntries": [
+ [
+ "",
+ "score_qcc_x86_64_toolchain_pkg",
+ "score_bazel_cpp_toolchains++gcc+score_qcc_x86_64_toolchain_pkg"
+ ],
+ [
+ "score_rules_imagefs+",
+ "bazel_tools",
+ "bazel_tools"
+ ],
+ [
+ "score_rules_imagefs+",
+ "score_rules_imagefs",
+ "score_rules_imagefs+"
+ ]
+ ]
+ }
}
},
"facts": {}
diff --git a/bazel/tools/BUILD b/bazel/tools/BUILD
new file mode 100644
index 00000000..ca5de742
--- /dev/null
+++ b/bazel/tools/BUILD
@@ -0,0 +1,12 @@
+# *******************************************************************************
+# Copyright (c) 2026 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# SPDX-License-Identifier: Apache-2.0
+# *******************************************************************************
diff --git a/bazel/tools/json_schema_validator.bzl b/bazel/tools/json_schema_validator.bzl
new file mode 100644
index 00000000..fb3c76fc
--- /dev/null
+++ b/bazel/tools/json_schema_validator.bzl
@@ -0,0 +1,57 @@
+"""
+Creates "validate_json_schema_test" test rule which validates the input "json" file against its "schema"
+"""
+
+visibility(["//..."])
+
+def _impl(ctx):
+ script = """
+readonly expected_failure={expected_failure}
+'{validator}' '{schema}' < '{json}'
+readonly ret=$?
+if test "$expected_failure" = true && test "$ret" -ne 0; then
+ exit 0
+elif test "$expected_failure" = false && test "$ret" -eq 0; then
+ exit 0
+fi
+echo "Test Failed: Json validator performed json validation of '{json}' against '{schema}' schema with exit code $ret, meanwhile rule attribute 'expected_failure' is equal to '{expected_failure}'.
+This means in case expected_failure=false then expected exit code should be zero (valid json). Otherwise, if expected_failure=true then exit code is non-zero (invalid json).
+Note: by default expected_failure is false."
+ exit 1
+ """.format(
+ validator = ctx.attr._validator.files_to_run.executable.short_path,
+ schema = ctx.file.schema.short_path,
+ json = ctx.file.json.short_path,
+ expected_failure = "true" if ctx.attr.expected_failure else "false",
+ )
+
+ ctx.actions.write(
+ output = ctx.outputs.executable,
+ content = script,
+ )
+
+ # To ensure the files needed by the script are available, put them in the runfiles
+ runfiles = ctx.runfiles(files = [ctx.file._validator, ctx.file.json, ctx.file.schema])
+ return [DefaultInfo(runfiles = runfiles)]
+
+validate_json_schema_test = rule(
+ implementation = _impl,
+ attrs = {
+ "expected_failure": attr.bool(default = False),
+ "json": attr.label(
+ allow_single_file = True,
+ mandatory = True,
+ ),
+ "schema": attr.label(
+ allow_single_file = True,
+ mandatory = True,
+ ),
+ "_validator": attr.label(
+ default = Label("@json_schema_validator"),
+ allow_single_file = True,
+ executable = True,
+ cfg = "exec",
+ ),
+ },
+ test = True,
+)
diff --git a/score/time_slave/config/qnx/BUILD b/score/time_slave/config/qnx/BUILD
new file mode 100644
index 00000000..4a8da602
--- /dev/null
+++ b/score/time_slave/config/qnx/BUILD
@@ -0,0 +1,16 @@
+# *******************************************************************************
+# Copyright (c) 2026 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# SPDX-License-Identifier: Apache-2.0
+# *******************************************************************************
+
+package(default_visibility = ["//visibility:public"])
+
+exports_files(["time_slave_qnx_config.json"])
diff --git a/score/time_slave/config/qnx/time_slave_qnx_config.json b/score/time_slave/config/qnx/time_slave_qnx_config.json
new file mode 100644
index 00000000..987a4f2a
--- /dev/null
+++ b/score/time_slave/config/qnx/time_slave_qnx_config.json
@@ -0,0 +1,18 @@
+{
+ "iface_name": "emac0",
+ "domain_number": 0,
+ "pdelay_req_interval_ms": 1000,
+ "pdelay_warmup_ms": 2000,
+ "sync_timeout_ms": 3300,
+ "jump_future_threshold_ns": 500000000,
+ "shm_path": "/gptp_shmem",
+ "phc": {
+ "enabled": true,
+ "device": "emac0",
+ "step_threshold_ns": 100000000
+ },
+ "qnx": {
+ "bpf_device_prefix": "/dev/bpf",
+ "see_sent": true
+ }
+}
diff --git a/score/time_slave/docs/manuals/index.rst b/score/time_slave/docs/manuals/index.rst
new file mode 100644
index 00000000..fa649c74
--- /dev/null
+++ b/score/time_slave/docs/manuals/index.rst
@@ -0,0 +1,21 @@
+..
+ # *******************************************************************************
+ # Copyright (c) 2026 Contributors to the Eclipse Foundation
+ #
+ # See the NOTICE file(s) distributed with this work for additional
+ # information regarding copyright ownership.
+ #
+ # This program and the accompanying materials are made available under the
+ # terms of the Apache License Version 2.0 which is available at
+ # https://www.apache.org/licenses/LICENSE-2.0
+ #
+ # SPDX-License-Identifier: Apache-2.0
+ # *******************************************************************************
+
+Manuals
+=======
+
+.. toctree::
+ :maxdepth: 1
+
+ qnx_setup
diff --git a/score/time_slave/docs/manuals/qnx_setup.rst b/score/time_slave/docs/manuals/qnx_setup.rst
new file mode 100644
index 00000000..04e88c8e
--- /dev/null
+++ b/score/time_slave/docs/manuals/qnx_setup.rst
@@ -0,0 +1,190 @@
+..
+ # *******************************************************************************
+ # Copyright (c) 2026 Contributors to the Eclipse Foundation
+ #
+ # See the NOTICE file(s) distributed with this work for additional
+ # information regarding copyright ownership.
+ #
+ # This program and the accompanying materials are made available under the
+ # terms of the Apache License Version 2.0 which is available at
+ # https://www.apache.org/licenses/LICENSE-2.0
+ #
+ # SPDX-License-Identifier: Apache-2.0
+ # *******************************************************************************
+
+Running TimeSlave on QNX
+########################
+
+This manual explains how to configure, build, and run the TimeSlave
+process on QNX (SDP 8.0.4, x86_64).
+
+Prerequisites
+*************
+
+* `QNX Software Center `_ installed on the
+ build host, with **QNX SDP 8.0** deployed.
+* A valid QNX license, placed under ``/var/tmp/.qnx/license/licenses``
+ (the QNX tools hard-code ``QNX_CONFIGURATION_EXCLUSIVE=/var/tmp/.qnx``;
+ see :doc:`/memory:qnx-license-var-tmp`).
+* `Bazel `_ 8.6.0 or later (see
+ ``tools/setup_toolchain.sh`` for an automated setup script).
+* QEMU with KVM support for running the reference integration test
+ (optional, but required for the QEMU-based integration test).
+* Bazel user credentials for ``qnx.com`` (used by
+ ``tools/qnx_credential_helper.py``) — set via ``~/.netrc`` or the
+ ``SCORE_QNX_USER`` / ``SCORE_QNX_PASSWORD`` environment variables.
+
+Configuration
+*************
+
+TimeSlave is configured via a JSON file. The schema is defined in
+``score/time_slave/src/application/configuration/time_slave_config_schema.json``
+and covers all runtime parameters: network interface, gPTP timing, PHC
+adjustment, shared-memory path, and QNX-specific options.
+
+Configuration file location
+===========================
+
+The configuration file is resolved in the following order:
+
+1. ``--config `` command-line argument.
+2. ``TIMESLAVE_CONFIG`` environment variable.
+3. ``./etc/time_slave_config.json`` relative to the process working
+ directory.
+
+If no configuration file is found at the resolved path, TimeSlave uses
+built-in defaults (equivalent to the schema defaults) and logs an
+informational message.
+
+For backwards compatibility, the ``GPTP_IFACE`` environment variable
+(when set) still overrides the ``iface_name`` field after the JSON config
+is loaded.
+
+QNX-specific fields
+===================
+
+The optional ``"qnx"`` section holds QNX-only settings (ignored on
+Linux):
+
+.. code-block:: json
+
+ {
+ "qnx": {
+ "bpf_device_prefix": "/dev/bpf",
+ "see_sent": true
+ }
+ }
+
+``bpf_device_prefix``
+ Path prefix for the BPF (Berkeley Packet Filter) devices used for raw
+ Ethernet frame capture. Defaults to ``/dev/bpf``.
+
+``see_sent``
+ When ``true``, sent frames are also delivered to the RX BPF. This is
+ required to capture TX hardware timestamps for Pdelay_Req T1
+ measurement. Defaults to ``false``.
+
+PHC configuration
+=================
+
+To enable PTP Hardware Clock adjustment, set ``"phc"."enabled"`` to
+``true``. On QNX the ``device`` field identifies the network interface
+whose EMAC PTP clock is adjusted (e.g. ``"emac0"``):
+
+.. code-block:: json
+
+ {
+ "phc": {
+ "enabled": true,
+ "device": "emac0",
+ "step_threshold_ns": 100000000
+ }
+ }
+
+When disabled (the default), gPTP runs in slave-only mode without
+disciplining a hardware clock.
+
+Example
+=======
+
+A representative QNX configuration is shipped at
+``score/time_slave/config/qnx/time_slave_qnx_config.json``:
+
+.. literalinclude:: ../../config/qnx/time_slave_qnx_config.json
+ :language: json
+
+Build steps
+***********
+
+Build for QNX x86_64
+====================
+
+.. code-block:: shell
+
+ # (Once) Source the setup helper to register QNX and toolchain paths.
+ source tools/setup_toolchain.sh
+
+ # Build TimeSlave for QNX x86_64.
+ bazel build --config=time-x86_64-qnx //score/time_slave:time_slave
+
+The resulting binary appears under ``bazel-bin/score/time_slave/src/application/time_slave``.
+
+Packaging for deployment
+========================
+
+To produce a deployable layout (``/opt/time_slave/bin/`` +
+``/opt/time_slave/etc/``) use the ``pkg_application`` macro defined in
+``score/time_slave/pkg_application.bzl``. A typical BUILD snippet:
+
+.. code-block:: python
+
+ load("//score/time_slave:pkg_application.bzl", "pkg_application")
+
+ pkg_application(
+ name = "time_slave-pkg",
+ app_name = "time_slave",
+ bin = ["//score/time_slave:time_slave"],
+ etc = ["//score/time_slave/config/qnx:time_slave_qnx_config.json"],
+ )
+
+The resulting ``pkg_filegroup`` can be fed to ``pkg_tar`` (for
+archive-based deployment), ``oci_image`` (for Docker), or ``qnx_ifs``
+(for inclusion in a QNX IFS image).
+
+Running on QNX
+**************
+
+On a QNX target (or in the QEMU reference integration), copy the binary
+and config to the conventional locations and run:
+
+.. code-block:: shell
+
+ # On the QNX target shell:
+ mkdir -p /opt/time_slave/etc
+ cp time_slave /opt/time_slave/bin/
+ cp time_slave_qnx_config.json /opt/time_slave/etc/time_slave_config.json
+ cd /opt/time_slave
+ ./bin/time_slave --config etc/time_slave_config.json
+
+To use a non-default interface without a config file:
+
+.. code-block:: shell
+
+ GPTP_IFACE=emac1 ./bin/time_slave
+
+Reference integration test
+**************************
+
+A reference integration test is provided at
+``score/time_slave/tests/reference_integration/``. It boots a minimal
+QNX QEMU image containing the TimeSlave binary and a QNX configuration
+file, starts the process, and verifies that it configures itself
+correctly, opens the shared-memory channel, and begins publishing gPTP
+snapshots.
+
+Run it with:
+
+.. code-block:: shell
+
+ bazel test --config=time-x86_64-qnx \
+ //score/time_slave/tests/reference_integration:time_slave_qnx_ref_integ
diff --git a/score/time_slave/src/application/BUILD b/score/time_slave/src/application/BUILD
index 34ed6ef2..022a3734 100644
--- a/score/time_slave/src/application/BUILD
+++ b/score/time_slave/src/application/BUILD
@@ -24,6 +24,7 @@ cc_binary(
tags = ["QM"],
visibility = ["//score/time_slave:__pkg__"],
deps = [
+ "//score/time_slave/src/application/configuration",
"//score/time_slave/src/common:logging_contexts",
"//score/time_slave/src/gptp:gptp_engine",
"//score/ts_client/src:gptp_ipc",
diff --git a/score/time_slave/src/application/configuration/BUILD b/score/time_slave/src/application/configuration/BUILD
new file mode 100644
index 00000000..e7561ff1
--- /dev/null
+++ b/score/time_slave/src/application/configuration/BUILD
@@ -0,0 +1,48 @@
+# *******************************************************************************
+# Copyright (c) 2026 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# SPDX-License-Identifier: Apache-2.0
+# *******************************************************************************
+
+load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES")
+load("//bazel/tools:json_schema_validator.bzl", "validate_json_schema_test")
+
+filegroup(
+ name = "time_slave_config_schema",
+ srcs = ["time_slave_config_schema.json"],
+ visibility = ["//visibility:public"],
+)
+
+cc_library(
+ name = "configuration",
+ srcs = ["config_parser.cpp"],
+ hdrs = [
+ "config_parser.h",
+ "time_slave_config.h",
+ ],
+ features = COMPILER_WARNING_FEATURES,
+ tags = ["QM"],
+ visibility = ["//score/time_slave:__subpackages__"],
+ deps = [
+ "//score/time_slave/src/common:logging_contexts",
+ "//score/time_slave/src/gptp:gptp_engine",
+ "@score_baselibs//score/json",
+ "@score_baselibs//score/json:json_parser",
+ "@score_baselibs//score/mw/log:console_only_backend",
+ ],
+)
+
+validate_json_schema_test(
+ name = "validate_time_slave_config_schema",
+ json = "example/time_slave_config.json",
+ schema = ":time_slave_config_schema",
+ tags = ["lint"],
+ target_compatible_with = ["@platforms//os:linux"],
+)
diff --git a/score/time_slave/src/application/configuration/config_parser.cpp b/score/time_slave/src/application/configuration/config_parser.cpp
new file mode 100644
index 00000000..0f758a9b
--- /dev/null
+++ b/score/time_slave/src/application/configuration/config_parser.cpp
@@ -0,0 +1,177 @@
+/********************************************************************************
+ * Copyright (c) 2026 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Apache License Version 2.0 which is available at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ ********************************************************************************/
+#include "score/time_slave/src/application/configuration/config_parser.h"
+
+#include "score/json/json_parser.h"
+#include "score/mw/log/logging.h"
+#include "score/time_slave/src/common/logging_contexts.h"
+
+#include
+#include
+#include
+
+namespace score
+{
+namespace ts
+{
+
+namespace
+{
+
+using score::json::Error;
+using score::json::Object;
+
+/// Fatal out if @p result is a type/parse error (as opposed to the caller having
+/// already detected "key not present" via Object::find()).
+template
+void FatalOnTypeError(const ResultT& result, std::string_view key)
+{
+ if (!result.has_value() && static_cast(*result.error()) != static_cast(Error::kKeyNotFound))
+ {
+ score::mw::log::LogFatal(kTimeSlaveAppContext)
+ << "Config field '" << key << "' has wrong type: " << result.error().Message();
+ }
+}
+
+/// Read a string field; leaves @p out unchanged if key is missing.
+void ParseOptionalString(const Object& obj, std::string_view key, std::string& out)
+{
+ auto it = obj.find(key);
+ if (it == obj.end())
+ {
+ return;
+ }
+ auto result = it->second.As();
+ FatalOnTypeError(result, key);
+ if (result.has_value())
+ {
+ out = std::string{result.value()};
+ }
+}
+
+/// Read an integer field; leaves @p out unchanged if key is missing.
+template
+void ParseOptionalInt(const Object& obj, std::string_view key, T& out)
+{
+ auto it = obj.find(key);
+ if (it == obj.end())
+ {
+ return;
+ }
+ auto result = it->second.As();
+ FatalOnTypeError(result, key);
+ if (result.has_value())
+ {
+ out = static_cast(result.value());
+ }
+}
+
+/// Read a bool field; leaves @p out unchanged if key is missing.
+void ParseOptionalBool(const Object& obj, std::string_view key, bool& out)
+{
+ auto it = obj.find(key);
+ if (it == obj.end())
+ {
+ return;
+ }
+ auto result = it->second.As();
+ FatalOnTypeError(result, key);
+ if (result.has_value())
+ {
+ out = result.value();
+ }
+}
+
+/// Parse the optional "phc" nested object. If absent, @p cfg keeps defaults.
+void ParsePhcConfig(const Object& obj, details::PhcConfig& cfg)
+{
+ auto it = obj.find("phc");
+ if (it == obj.end())
+ {
+ return;
+ }
+ auto obj_result = it->second.As