diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 0000000..a7ef843 --- /dev/null +++ b/.bazelrc @@ -0,0 +1,14 @@ +build --java_language_version=17 +build --tool_java_language_version=17 +build --java_runtime_version=remotejdk_17 +build --tool_java_runtime_version=remotejdk_17 +build --@score-baselibs//score/json:base_library=nlohmann +build --@score-baselibs//score/mw/log/flags:KRemote_Logging=False + +test --test_output=errors + +common --registry=https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/ +common --registry=https://bcr.bazel.build + +# allow empty globs for docs +build --noincompatible_disallow_empty_glob diff --git a/.bazelversion b/.bazelversion new file mode 100644 index 0000000..2bf50aa --- /dev/null +++ b/.bazelversion @@ -0,0 +1 @@ +8.3.0 diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..1d31456 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,97 @@ +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", +] + +line-length = 120 +indent-width = 4 + +[lint] +select = [ + # flake8-boolean-trap + "FBT", + # flake8-bugbear + "B", + # flake8-builtins + "A", + # flake8-comprehensions + "C4", + # flake8-fixme + "FIX", + # flake8-implicit-str-concat + "ISC", + # flake8-pie + "PIE", + # flake8-print + "T20", + # flake8-pytest-style + "PT", + # flake8-raise + "RSE", + # flake8-return + "RET501", + "RET502", + "RET503", + "RET504", + # flake8-self + "SLF", + # flake8-simplify + "SIM", + # flake8-type-checking + "TC", + # flake8-unused-arguments + "ARG", + # flake8-use-pathlib + "PTH", + + # isort + "I", + + # pycodestyle error + "E", + # Pyflakes + "F", + # pyupgrade + "UP", +] +ignore = ["F401", "PTH123", "ARG002"] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" \ No newline at end of file diff --git a/.yamlfmt b/.yamlfmt new file mode 100644 index 0000000..26775cb --- /dev/null +++ b/.yamlfmt @@ -0,0 +1,3 @@ +formatter: + type: basic + retain_line_breaks: true \ No newline at end of file diff --git a/BUILD b/BUILD new file mode 100644 index 0000000..ad7c57d --- /dev/null +++ b/BUILD @@ -0,0 +1,59 @@ +# ******************************************************************************* +# Copyright (c) 2025 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_cr_checker//:cr_checker.bzl", "copyright_checker") +load("@score_dash_license_checker//:dash.bzl", "dash_license_checker") +load("@score_docs_as_code//:docs.bzl", "docs") +load("@score_format_checker//:macros.bzl", "use_format_targets") +load("@score_starpls_lsp//:starpls.bzl", "setup_starpls") +load("//:project_config.bzl", "PROJECT_CONFIG") + +# Creates all documentation targets: +# - `docs:incremental` for building docs incrementally at runtime +# - `docs:live_preview` for live preview in the browser without an IDE + +# - `docs:docs` for building documentation at build-time +docs( + data = [ + "@score_platform//:needs_json", + "@score_process//:needs_json", + ], + source_dir = "docs", +) + +setup_starpls( + name = "starpls_server", + visibility = ["//visibility:public"], +) + +copyright_checker( + name = "copyright", + srcs = [ + "src", + "tests", + "//:BUILD", + "//:MODULE.bazel", + ], + config = "@score_cr_checker//resources:config", + template = "@score_cr_checker//resources:templates", + visibility = ["//visibility:public"], +) + +dash_license_checker( + src = "//examples:cargo_lock", + file_type = "", # let it auto-detect based on project_config + project_config = PROJECT_CONFIG, + visibility = ["//visibility:public"], +) + +# Add target for formatting checks +use_format_targets() diff --git a/Cargo.toml b/Cargo.toml index d1b5b1e..ddefb5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,28 @@ -[package] -name = "fault-lib" -version = "0.1.0" +[workspace] +resolver = "2" + +members = [ + "src/common", + "src/dfm_lib", + "src/fault_lib", + "src/xtask", +] + +[workspace.package] +version = "0.0.1" edition = "2024" +license-file = "LICENSE.md" +authors = ["S-CORE Contributors"] +readme = "README.md" -[dependencies] -thiserror = "2" \ No newline at end of file +[workspace.dependencies] +env_logger = "0.11.8" +iceoryx2 = { git = "https://github.com/eclipse-iceoryx/iceoryx2.git", rev = "eba5da4b8d8cb03bccf1394d88a05e31f58838dc"} +iceoryx2-bb-container = { git = "https://github.com/eclipse-iceoryx/iceoryx2.git", rev = "eba5da4b8d8cb03bccf1394d88a05e31f58838dc"} +log = "0.4.22" +mockall = "0.13.1" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sha2 = "0.10" +thiserror = "2.0.17" +xtask = { path = "src/xtask" } diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 0000000..206f487 --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,113 @@ +# ******************************************************************************* +# Copyright (c) 2025 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 +# ******************************************************************************* +module( + name = "score_fault_lib", + version = "0.1.0", + compatibility_level = 0, +) + +bazel_dep(name = "rules_python", version = "1.4.1") + +PYTHON_VERSION = "3.12" + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain( + is_default = True, + python_version = PYTHON_VERSION, +) +use_repo(python) + +# Add GoogleTest dependency +bazel_dep(name = "googletest", version = "1.17.0") +bazel_dep(name = "google_benchmark", version = "1.9.4") + +# Rust rules for Bazel +bazel_dep(name = "rules_rust", version = "0.67.0") + +# Checker rule for CopyRight checks/fixs +bazel_dep(name = "score_cr_checker", version = "0.3.1") + +# C/C++ rules for Bazel +bazel_dep(name = "rules_cc", version = "0.2.13") + +# LLVM Toolchains Rules - host configuration +bazel_dep(name = "score_starpls_lsp", version = "0.1.0") + +# Dash license checker +bazel_dep(name = "score_dash_license_checker", version = "0.1.2") + +# Format checker +bazel_dep(name = "score_format_checker", version = "0.1.1") +bazel_dep(name = "aspect_rules_lint", version = "1.4.4") +bazel_dep(name = "buildifier_prebuilt", version = "8.2.0.2") + +#docs-as-code +bazel_dep(name = "score_docs_as_code", version = "1.0.1") +git_override( + module_name = "score_docs_as_code", + commit = "13ba715a95cfe85158b60d7f4748ba8e28895d8c", + remote = "https://github.com/eclipse-score/docs-as-code.git", +) + +# Provides, pytest & venv +bazel_dep(name = "score_python_basics", version = "0.3.4") +bazel_dep(name = "score_platform", version = "0.3.0") +bazel_dep(name = "score_process", version = "1.1.0") + +# Testing utils dependency. +# Direct usage of tag in git_override reports false problem in editor, using hash of a tag +bazel_dep(name = "testing-utils") +git_override( + module_name = "testing-utils", + commit = "a847c7464cfa47e000141631d1223b92560d2e58", # tag v0.2.0 + remote = "https://github.com/qorix-group/testing_tools.git", +) + +# Module deps +rust = use_extension("@rules_rust//rust:extensions.bzl", "rust") +rust.toolchain( + edition = "2024", + versions = ["1.91.0"], +) + +crate = use_extension("@rules_rust//crate_universe:extensions.bzl", "crate") +crate.from_cargo( + name = "score_fault_lib_crates", + cargo_lockfile = "//:Cargo.lock", + manifests = [ + "//:Cargo.toml", + ], +) +use_repo(crate, "score_fault_lib_crates") + +#bazel_dep on module 'rules_boost' has no version -> override needed +archive_override( + module_name = "rules_boost", + strip_prefix = "rules_boost-master", + urls = ["https://github.com/nelhage/rules_boost/archive/refs/heads/master.tar.gz"], +) + +bazel_dep(name = "score-baselibs", version = "0.0.0") +git_override( + module_name = "score-baselibs", + commit = "46923f5c4f302bd9feae0261588687aaf32e3c5c", + remote = "https://github.com/eclipse-score/baselibs.git", +) + +bazel_dep(name = "score_cli_helper", version = "0.1.2") + +crate.annotation( + crate = "iceoryx2-bb-derive-macros", + patches = ["//patches:iceoryx2_bb_derive_macros_readme.patch"], + repositories = ["score_fault_lib_crates"], +) diff --git a/README.md b/README.md index c830476..fc45260 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,161 @@ - - -# Fault Library - -OpenSOVD Fault Library - -## Design - -The high-level design can be found here: [OpenSOVD Design](https://github.com/eclipse-opensovd/opensovd/blob/main/docs/design/design.md) - -The Fault Lib design can be found here: [Fault Lib Design](docs/design/design.md) + +# Diagnostic Fault Library + +This repository contains Diagnostic Fault library + +--- + +## πŸ“‚ Project Structure + +| File/Folder | Description | +| ----------------------------------- | ------------------------------------------------- | +| `README.md` | Short description & build instructions | +| `src/` | Source files for the module | +| `tests/` | Unit tests (UT) and integration tests (IT) | +| `examples/` | Example files used for guidance | +| `docs/` | Documentation (Doxygen for C++ / mdBook for Rust) | +| `.github/workflows/` | CI/CD pipelines | +| `.vscode/` | Recommended VS Code settings | +| `.bazelrc`, `MODULE.bazel`, `BUILD` | Bazel configuration & settings | +| `project_config.bzl` | Project-specific metadata for Bazel macros | +| `LICENSE.md` | Licensing information | +| `CONTRIBUTION.md` | Contribution guidelines | + +--- + +## πŸš€ Getting Started + +### 1️⃣ Clone the Repository + +```sh +git clone https://github.com/eclipse-score/inc_diag.git +cd inc_diag +``` + +### 2️⃣ Build the Examples of module + +To build the example showing how to use the fault-lib use: + +```sh +bazel build //src/fault-lib:fault-lib-basic-ex +``` + +or directly run it with: + +```sh +bazel run //src/fault-lib:fault-lib-basic-ex +``` + +To build all targets of the module the following command can be used: + +```sh +bazel build //src/... +``` + +This command will instruct Bazel to build all targets that are under Bazel +package `src/`. The ideal solution is to provide single target that builds +artifacts, for example: + +```sh +bazel build //src/:release_artifacts +``` + +where `:release_artifacts` is filegroup target that collects all release +artifacts of the module. + +> NOTE: This is just proposal, the final decision is on module maintainer how +> the module code needs to be built. + + + +### 3️⃣ Run Tests + +To get the fault lib example with the test instance of the diagnostic fault manager side running do the following steps. +Start the test version of the Diagnostic Fault manager: +```sh +cargo run --example=dfm +``` + +The `dfm` process uses hardcoded fault catalogs (only to show the possibility), which are equal to the example fault catalog json files stored under `src/fault_lib/tests/data/`. +When the `dfm` is running you should see the following log message +```sh +[2026-01-04T20:46:11Z INFO dfm_lib::fault_lib_communicator] FaultLibCommunicator listening... +``` + +Now the reporting application can be started. For that call in new console: + +```sh +cargo run --bin tst_app -- -c src/fault_lib/tests/data/ivi_fault_catalog.json +``` + +and / or + +```sh +cargo run --bin tst_app -- -c src/fault_lib/tests/data/hvac_fault_catalog.json +``` + +The `tst_app` process reads the fault catalog and loops 20 times over all the fault's IDs present in the catalog. +In each loop the `tst_app` uses `fault-lib` API and reports the faults either to be pass or failed with small delay (200ms) between loops. + +You should be able to see the `dfm` process reporting the faults to be received and stored, e.g.: + +``` +[2026-01-04T20:48:37Z INFO dfm_lib::fault_lib_communicator] Received new fault ID: Numeric(28673) +[2026-01-04T20:48:37Z INFO dfm_lib::fault_record_processor] Fault ID Numeric(28673) stored : true +[2026-01-04T20:48:37Z INFO dfm_lib::fault_lib_communicator] Received new fault ID: Text(StaticString<64> { len: 33, data: "hvac.blower.speed_sensor_mismatch" }) +[2026-01-04T20:48:37Z INFO dfm_lib::fault_record_processor] Fault ID Text(StaticString<64> { len: 33, data: "hvac.blower.speed_sensor_mismatch" }) stored : true +[2026-01-04T20:48:38Z INFO dfm_lib::fault_lib_communicator] Received new fault ID: Text(StaticString<64> { len: 2, data: "d1" }) +[2026-01-04T20:48:38Z INFO dfm_lib::fault_record_processor] Fault ID Text(StaticString<64> { len: 2, data: "d1" }) stored : true +[2026-01-04T20:48:38Z INFO dfm_lib::fault_lib_communicator] Received new fault ID: Text(StaticString<64> { len: 2, data: "d2" }) +[2026-01-04T20:48:38Z INFO dfm_lib::fault_record_processor] Fault ID Text(StaticString<64> { len: 2, data: "d2" }) stored : true +[2026-01-04T20:48:38Z INFO dfm_lib::fault_lib_communicator] Received new fault ID: Numeric(28673) +``` + + +--- + +## πŸ›  Tools & Linters + +The template integrates **tools and linters** from **centralized repositories** to ensure consistency across projects. + +- **C++:** `clang-tidy`, `cppcheck`, `Google Test` +- **Rust:** `clippy`, `rustfmt`, `Rust Unit Tests` +- **CI/CD:** GitHub Actions for automated builds and tests + +--- + +## πŸ“– Documentation + +To run localy the live preview of the documentation: + +```sh +bazel run //docs:live_preview +``` + + +--- + +## βš™οΈ `project_config.bzl` + +This file defines project-specific metadata used by Bazel macros, such as `dash_license_checker`. + +### πŸ“Œ Purpose + +It provides structured configuration that helps determine behavior such as: + +- Source language type (used to determine license check file format) +- Safety level or other compliance info (e.g. ASIL level) + +### πŸ“„ Example Content + +```python +PROJECT_CONFIG = { + "asil_level": "QM", # or "ASIL-A", "ASIL-B", etc. + "source_code": ["cpp", "rust"] # Languages used in the module +} +``` + +### πŸ”§ Use Case + +When used with macros like `dash_license_checker`, it allows dynamic selection of file types + (e.g., `cargo`, `requirements`) based on the languages declared in `source_code`. diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..8ae2add --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,65 @@ +# ******************************************************************************* +# Copyright (c) 2024 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 +# ******************************************************************************* + +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = "Module Template Project" +project_url = "https://eclipse-score.github.io/inc_diag/" +project_prefix = "DIAG_" +author = "S-CORE" +version = "0.1.0" + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + + +extensions = [ + "sphinx_design", + "sphinx_needs", + "sphinxcontrib.plantuml", + "score_plantuml", + "score_metamodel", + "score_draw_uml_funcs", + "score_source_code_linker", + "score_layout", +] + +myst_enable_extensions = ["colon_fence"] + +exclude_patterns = [ + # The following entries are not required when building the documentation via 'bazel + # build //docs:docs', as that command runs in a sandboxed environment. However, when + # building the documentation via 'bazel run //docs:incremental' or esbonio, these + # entries are required to prevent the build from failing. + "bazel-*", + ".venv_docs", +] + +# Enable markdown rendering +source_suffix = { + ".rst": "restructuredtext", + ".md": "markdown", +} + + +templates_path = ["templates"] + +# Enable numref +numfig = True diff --git a/docs/drawings/lib_arch.drawio b/docs/drawings/lib_arch.drawio new file mode 100644 index 0000000..4b56bf9 --- /dev/null +++ b/docs/drawings/lib_arch.drawio @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/drawings/lib_arch.svg b/docs/drawings/lib_arch.svg new file mode 100644 index 0000000..8a52b23 --- /dev/null +++ b/docs/drawings/lib_arch.svg @@ -0,0 +1 @@ +
FaultMgrClient
FaultMgrClient
Fault Lib
Fault Lib
Diagnostic Fault ManagerΒ 
Diagnostic Fault Man...
Diagnostic DB
Diagnostic...
SOVD Gateway
SOVD Gateway
App
App
Fault Monitor
Fault Monitor
Enabling
Condition
Enabling...
Activity
Activity
Fault Lib
Fault Lib
.json
.json
.json
.json
.json
.json
.json
.json
Fault Catalog
Cfg
Fault Cata...
Faults Catalog
Faults Catalog
.json
.json
Fault Catalog
Cfg
Fault Cata...
Fault
Fault
Daignostic
Entity
Daignostic...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..3866abe --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,293 @@ +.. + # ************************************************************************** + # Copyright (c) 2024 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 + # ************************************************************************** + + +Diagnostic Fault Library Documentation +======================================= + +This documentation describes the structure, usage and configuration of the +Diagonostic Fault Library. + +.. contents:: Table of Contents + :depth: 4 + :local: + + + +Abbreviations +------------- + ++-------------+---------------------------+ +| **Abbrev.** | **Meaning** | ++=============+===========================+ +| FL | Fault Library | ++-------------+---------------------------+ +| DFM | Diagnostic Fault Manager | ++-------------+---------------------------+ +| FOTA | Flashing over the air | ++-------------+---------------------------+ +| IPC | Inter Process Com. | ++-------------+---------------------------+ +| HPC | High-Performance Computer | ++-------------+---------------------------+ + +Overview +-------- + +The diagnostic fault library should provide S-CORE applications with an API for reporting results of the diagnostic tests. +For more information on the SOVD context, see `S-CORE Diagnostic and Fault Management `_ + +Every application which is able to test health states of part or the complete HPC, it's submodules, hardware etc., needs +possibility to report results of those test back to the car environment, so the other application +or SOVD clients can access them. The Fault Library enables this possibility. + +The results of the tests reported to the FaultLib are send to the Diagnostic Fault Manager which stores or update them in the Diagnostic Data Base. + +.. image:: drawings/lib_arch.svg + :alt: Fault monitor + :width: 800px + :align: center + + +Mapping of the above modules names to the proposal from SOVD: + +- FaultMonitor -> Reporter +- FaultMgrClient -> FaultSink +- FaultApi -> FaultLib + + +Fault-lib and fault diagnostic manager +-------------------------------------- + +The fault diagnostic manager is a proxy between the apps reporting faults and the SOVD server. +Beside that it collects all faults in the system and manage persisten storage of their states. +According to the SOVD specification (chapter 4.3.1), faults can be reported by: + +- SOVD Server itself +- Component +- an App + + +Design Decisons & Trade-offs +---------------------------- + +Fault Catalog +~~~~~~~~~~~~~ +Despite the SOVD assumes to work with offline diagnostic services and faults catalogs (like ODX, etc.), we assume the fault_lib and DFM to share common fault catalogs. +Otherwise during the startup phase, all the fault_lib clients would need to register thousands of faults, which then would lead to heavy IPC traffic in the system. +Considering, that the presence of most of the faults in the car, doesn't change over the lifetime, it makes less sense to dynamically inform DFM about their existence by each startup. + +From another hand, there will be still a subset of the faults which cannot be known during the integration of the system, or can appear and disappear depending +on the current conditions in the car (change in the features configuration, OTA, new apps downloaded to the car , etc.). For that reason the fault_lib and the +DFM shall still provide mechanism which allow the FL client to register new faults and start to reporting resuls. + +.. note:: + TBD: + Do we need a mechanism to remove from DFM a fault in case it is not tested any more ? What the SOVD standard is expecting ? + +.. note:: + TBD: + How the fault catalog shall be looks like (generated code ? , json file (probably)), and be shared between DFM and FL + + +Use cases +--------- + +Following usecases are valid for the S-CORE application using the Fault Library: + +- registering new fault in the system + - depending on car configuration variant, enabled features etc. the number of faults detected and reported by the app can change + - depending on the current status and state of the car electronic system the APP can report different faults +- configuring debouncing for the fault + - different test can require the results to be filtered over time or debounced, to prevent setting the faults by glitches or false positives +- configuring enabling conditions for the fault + - each test can require different system conditions to be fulfilled before the test can be performed (e.g. the communication test can be done only if the power supply is in expected range) +- reporting results of diagnostic tests (fail / pass) +- reporting status of enabling conditions (if done in the app) + - the application can report only status on the enabling condition and does not report any faults +- react to the SOVD Fault Handling actions (e.g. delete faults can cause the test to restart) +- react to change in the enabling conditions (some tests could be impossible to be process when enabling conditions are not fulfilled) +- provide interface to the user which allow to provide additional environmental data to be stored with the fault + +Following usecases applies for the Fault Library (FL) and Diagnostic Fault Manager (DFM): + +- validate the consistency of the fault catalog shared between DFM and FL +- DFM maintain global fault catalog based on the information from each FL +- FL reports state changes in the faults to DFM over IPC +- FL reports enabling condition state change to the DFM over IPC +- DFM reports over IPC to FL enabling condition state change reported by another FL +- DFM requests restart of the test for the faults reported by FL +- DFM reports cleaning of the faults in the DFM by the SOVD client +- DFM receives and maintain current status of the environment conditions to be stored together with faults + + +Fault Catalog Init +~~~~~~~~~~~~~~~~~~~ + +This sequence shall ran at each start of the system to assure the FL and DFM are using consistent definitions of the faults. + +.. image:: puml/fault_catalog.svg + :alt: Fault monitor + :width: 800px + :align: center + + +New fault in the system +~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: puml/new_fault.svg + :alt: Fault monitor + :width: 1200px + :align: center + + +New enabling condition in the system +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: puml/new_enable_condition.svg + :alt: Fault enabling condition + :width: 800px + :align: center + + +Enabling conditions change +~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. image:: puml/enable_condition_ntf.svg + :alt: Fault enabling condition + :width: 1200px + :align: center + +Local Enabling Condition +~~~~~~~~~~~~~~~~~~~~~~~~ +.. image:: puml/local_enable_condition_ntf.svg + :alt: Fault enabling condition + :width: 1200px + :align: center + + +Diagnostic Fault Manager +------------------------ + + +Based on the above use cases, the Diagnostic Fault Manager shall: + +- collect and manage the fault's enabling conditions + - let all fault library instances to subscribe on the changes to the fault's enable conditions set + - notify all subscribes in case the set of the active enabling conditions changes + - let registering new enable conditions + - receive status of enable conditions and notify all fault_lib instances using those conditions +- collect and manage states of the faults + - handle registering of new faults in the system + - preventing duplication of the faults + - storing fault statuses reported by the apps + - storing information which fault reporter awaits which enabling condition notification +- notify fault lib instances about the fault's events triggered by the SOVD diagnostic server (e.g. delete fault, disable fault, trigger etc) + + +MVP +--- + +Scope +~~~~~ + +The MVP shall provide following functionality and features: + +- 2 test apps which can report faults to the Diagnostic Fault Manager over IPC +- in the first step no enabling conditions handling +- the fault catalog will be stored in the single json file and read by both FaultLibs and DiagnosticFaultManager +- the IPC from communication gateway shall be reused + +Design +~~~~~~ + +FaultCatalog +^^^^^^^^^^^^ + +The FaultCatalog module will read, validate the catalog configuration json file and create collection of available Faults with their properties. +Later on it will calculate the hashsum over the catalog and verify if the Diagnostic Fault Manager usees the same catalog. +If not, the Diagnostic Fault Manager will copy the FaultCatalog from the FL and update local copy (TBD: shoul DFM simply share FaultCatalog with FL ?) + + +Fault +^^^^^ + +The struct containing unique ID bound to the full fault property description in the Fault Catalog. The Fault-Lib will transfer to the DFM only this ID +to inform about fault status. All other information needed by SOVD server will be read by the DFM from Fault Catalog. + +Diagnostic Entity +^^^^^^^^^^^^^^^^^ +Keeps the information abut the SOVD entity to which the reported fault belongs. This is open topic. Unclear how the SOVD entities shall be managed and linked to the faults. + + + + + + + + + +Requirements +------------ + +.. stkh_req:: Example Functional Requirement + :id: stkh_req__docgen_enabled__example + :status: valid + :safety: QM + :security: YES + :reqtype: Functional + :rationale: Ensure documentation builds are possible for all modules + + +Project Layout +-------------- + +The module template includes the following top-level structure: + +- `src/`: Main C++/Rust sources +- `tests/`: Unit and integration tests +- `examples/`: Usage examples +- `docs/`: Documentation using `docs-as-code` +- `.github/workflows/`: CI/CD pipelines + +Quick Start +----------- + + +To build the module: + +.. code-block:: bash + + bazel build //src/... + +To run tests: + +.. code-block:: bash + + bazel test //tests/... + +Configuration +------------- + +The `project_config.bzl` file defines metadata used by Bazel macros. + +Example: + +.. code-block:: python + + PROJECT_CONFIG = { + "asil_level": "QM", + "source_code": ["cpp", "rust"] + } + +This enables conditional behavior (e.g., choosing `clang-tidy` for C++ or `clippy` for Rust). diff --git a/docs/puml/Registering new fault in the system.svg b/docs/puml/Registering new fault in the system.svg new file mode 100644 index 0000000..d669a61 --- /dev/null +++ b/docs/puml/Registering new fault in the system.svg @@ -0,0 +1 @@ +AppFaultMonitorFaultLibDiagFaultMgrAppAppFaultMonitorFaultMonitorFaultLibFaultLibDiagFaultMgrDiagFaultMgrStart upipc::subscribeEnablingConditions()ipc::ntfEnablingConditions(list[])get_fault(sovd_path,id,debouncing,enabling_conditions[])register_fault(sovd_path,enabling_conditions[])alt[Fault already registered]Fault already registeredor other conditions not okError()NoneOk(fault_id)Some<Fault(fault_id)>FaultMonitor::new(Fault)register()store(fault_monitor)App Reporting Faultreport_status(pass/fail)report(fault_id, status)ipc::report_fault_status(id, status) \ No newline at end of file diff --git a/docs/puml/enable_condition_ntf.puml b/docs/puml/enable_condition_ntf.puml new file mode 100644 index 0000000..57022e4 --- /dev/null +++ b/docs/puml/enable_condition_ntf.puml @@ -0,0 +1,49 @@ +@startuml +title Enabling Condition : local status notifications +box App +participant App1 +participant "EnablingCondition[id=XXX]" +participant FaultLib1 +end box + +box App2 +participant App2 +participant FaultMonitor +participant FaultLib2 +end box + +box "DFM" +participant DiagFaultMgr +end box + + + + +App2 -> FaultLib2 : get_fault(sovd_path,id,debouncing,enabling_conditions[]) +App2 <-- FaultLib2 : Some + +create FaultMonitor +App2 -> FaultMonitor : FaultMonitor::new(Fault, enabling_conditions, callback) + +FaultMonitor -> FaultLib2 : register() +FaultLib2 -> FaultLib2 : store(fault_monitor) +note right of FaultLib2 + The FL has to keep locally the track which fault + monitors are subscribing on which enabling conditions +end note + + + + +App1 -> "EnablingCondition[id=XXX]" : report_status(active/passive) +"EnablingCondition[id=XXX]" -> FaultLib1 : report_status() +FaultLib1 -> DiagFaultMgr : ipc::reportEnablingCondition(id=XXX, pass/fail) +FaultLib2 <- DiagFaultMgr : ipc::notifyEnablingCondtion(id=XXX, pass/fail) +FaultMonitor <-FaultLib2 : notify_ec_change(id=XXX, pass/fail) +App2 <- FaultMonitor : ntf_condition_change(EnablingContion[id=XXX],state) +note right of FaultMonitor + There will be applications + which want to be notified when + the enabling conditions has changed +end note +@enduml \ No newline at end of file diff --git a/docs/puml/enable_condition_ntf.svg b/docs/puml/enable_condition_ntf.svg new file mode 100644 index 0000000..e96588a --- /dev/null +++ b/docs/puml/enable_condition_ntf.svg @@ -0,0 +1 @@ +Enabling Condition : local status notificationsEnabling Condition : local status notificationsAppApp2DFMApp1EnablingCondition.id.XXX.FaultLib1App2FaultMonitorFaultLib2DiagFaultMgrApp1App1EnablingCondition[id=XXX]EnablingCondition[id=XXX]FaultLib1FaultLib1App2App2FaultMonitorFaultLib2FaultLib2DiagFaultMgrDiagFaultMgrget_fault(sovd_path,id,debouncing,enabling_conditions[])Some<Fault(fault_id)>FaultMonitor::new(Fault, enabling_conditions, callback)FaultMonitorregister()store(fault_monitor)The FL has to keep locally the track which faultmonitors are subscribing on which enabling conditionsreport_status(active/passive)report_status()ipc::reportEnablingCondition(id=XXX, pass/fail)ipc::notifyEnablingCondtion(id=XXX, pass/fail)notify_ec_change(id=XXX, pass/fail)ntf_condition_change(EnablingContion[id=XXX],state)There will be applicationswhich want to be notified whenthe enabling conditions has changed \ No newline at end of file diff --git a/docs/puml/fault_catalog.puml b/docs/puml/fault_catalog.puml new file mode 100644 index 0000000..db6c0c8 --- /dev/null +++ b/docs/puml/fault_catalog.puml @@ -0,0 +1,56 @@ +@startuml +title Fault Catalog Initialization + +skinparam ParticipantPadding 20 + +box "App" +participant App order 1 +create FaultLib order 2 +App -> FaultLib : instantiate +end box + +box "DFM" +participant DiagnosticFaultMgr order 3 +end box + + +FaultLib -> FaultLib : read_and_verify_catalog_config_file() +FaultLib -> FaultLib : calculate_catalog_hashsum() +FaultLib -> DiagnosticFaultMgr : ipc::VerifyFaultCatalogRequest(hashsum) +alt + note right of DiagnosticFaultMgr + The Fault Catalog is consistent with the + one present in Diagnostic Fault Manager (DFM) + end note + FaultLib <-- DiagnosticFaultMgr : ipc::Ok() + FaultLib -> FaultLib : FaultCatalogOk() +else + note right of DiagnosticFaultMgr + The Fault Catalog is not consistent with the + one present in Diagnostic Fault Manager + end note + FaultLib <-- DiagnosticFaultMgr : ipc::Error() + FaultLib -> DiagnosticFaultMgr : ipc::UpdateFaultCatalogRequest(faultCatalog) + note right of DiagnosticFaultMgr + The DFM updates local Fault catalog, + based on the info received from the app. + end note + DiagnosticFaultMgr -> DiagnosticFaultMgr : updateLocalDB(faultCatalog) + alt + note right of DiagnosticFaultMgr + In case the update of the fault + catalog fails on DFM side, there will be no recovery. + FaultDiagnosticManager shall report internal error !!! + end note + DiagnosticFaultMgr -> DiagnosticFaultMgr : enterErrorState() + FaultLib <--DiagnosticFaultMgr : ipc::Error() + + else + FaultLib <--DiagnosticFaultMgr : ipc::Ok() + FaultLib -> FaultLib : FaultCatalogOk() + end + +end + + +@enduml \ No newline at end of file diff --git a/docs/puml/fault_catalog.svg b/docs/puml/fault_catalog.svg new file mode 100644 index 0000000..df41efa --- /dev/null +++ b/docs/puml/fault_catalog.svg @@ -0,0 +1 @@ +Fault Catalog InitializationFault Catalog InitializationAppDFMAppFaultLibDiagnosticFaultMgrAppAppFaultLibDiagnosticFaultMgrDiagnosticFaultMgrinstantiateFaultLibread_and_verify_catalog_config_file()calculate_catalog_hashsum()ipc::VerifyFaultCatalogRequest(hashsum)altThe Fault Catalog is consistent with theone present in Diagnostic Fault Manager (DFM)ipc::Ok()FaultCatalogOk()The Fault Catalog is not consistent with theone present in Diagnostic Fault Manageripc::Error()ipc::UpdateFaultCatalogRequest(faultCatalog)The DFM updates local Fault catalog,based on the info received from the app.updateLocalDB(faultCatalog)altIn case the update of the faultcatalog fails on DFM side, there will be no recovery.FaultDiagnosticManager shall report internal error !!!enterErrorState()ipc::Error()ipc::Ok()FaultCatalogOk() \ No newline at end of file diff --git a/docs/puml/local_enable_condition_ntf.puml b/docs/puml/local_enable_condition_ntf.puml new file mode 100644 index 0000000..2a43433 --- /dev/null +++ b/docs/puml/local_enable_condition_ntf.puml @@ -0,0 +1,43 @@ + + +@startuml +title Enabling Condition : status notifications between activities or apps +box App1 +participant App +participant "EnablingCondition[id=XXX]" +participant FaultMonitor +participant FaultLib +end box + +box "DFM" +participant DiagFaultMgr +end box + + +ref over "EnablingCondition[id=XXX]", FaultLib: [[new_enable_condition.svg New Enable Condition]] + +App -> FaultLib : get_fault(sovd_path,id,debouncing,enabling_conditions[]) +App <-- FaultLib : Some + +create FaultMonitor +App -> FaultMonitor : FaultMonitor::new(Fault, enabling_conditions, callback) + +FaultMonitor -> FaultLib : register() +FaultLib -> FaultLib : store(fault_monitor) +note right of FaultLib + The FL has to keep locally the track which fault + monitors are subscribing on which enabling conditions +end note + + + + +App -> "EnablingCondition[id=XXX]" : report_status(pass/fail) +"EnablingCondition[id=XXX]" -> FaultLib : report_status() + +FaultLib -> DiagFaultMgr : ipc::notifyEnablingCondition(id=XXX, pass/fail) +FaultMonitor <-FaultLib : notify_ec_change(id=XXX, pass/fail) +App <- FaultMonitor : ntf_condition_change(EnablingCondition[id=XXX],state) + +@enduml + diff --git a/docs/puml/local_enable_condition_ntf.svg b/docs/puml/local_enable_condition_ntf.svg new file mode 100644 index 0000000..dceddd9 --- /dev/null +++ b/docs/puml/local_enable_condition_ntf.svg @@ -0,0 +1 @@ +Enabling Condition : status notifications between activities or appsEnabling Condition : status notifications between activities or appsApp1DFMAppEnablingCondition.id.XXX.FaultMonitorFaultLibDiagFaultMgrAppAppEnablingCondition[id=XXX]EnablingCondition[id=XXX]FaultMonitorFaultLibFaultLibDiagFaultMgrDiagFaultMgrrefNew Enable Conditionget_fault(sovd_path,id,debouncing,enabling_conditions[])Some<Fault(fault_id)>FaultMonitor::new(Fault, enabling_conditions, callback)FaultMonitorregister()store(fault_monitor)The FL has to keep locally the track which faultmonitors are subscribing on which enabling conditionsreport_status(pass/fail)report_status()ipc::notifyEnablingCondition(id=XXX, pass/fail)notify_ec_change(id=XXX, pass/fail)ntf_condition_change(EnablingCondition[id=XXX],state) \ No newline at end of file diff --git a/docs/puml/new_enable_condition.puml b/docs/puml/new_enable_condition.puml new file mode 100644 index 0000000..d64aba9 --- /dev/null +++ b/docs/puml/new_enable_condition.puml @@ -0,0 +1,39 @@ +@startuml +title Registering new enable condition by the provider +box "App" +participant App +participant EnablingCondition +participant FaultLib +end box +box "DFM" +participant DiagFaultMgr +end box + +== Startup == +FaultLib -> DiagFaultMgr : ipc::subscribeEnablingConditions() +FaultLib <- DiagFaultMgr : ipc::ntfEnablingConditions(list[]) + +App -> FaultLib : get_enabling_condition(sovd_entity) +FaultLib -> FaultLib : check_if_exist(sovd_entity) + +alt [ "EnablingCondition already exist or error"] + App <-- FaultLib : Error +else + + create EnablingCondition + EnablingCondition <- FaultLib : new::EnablingCondition() + FaultLib -> DiagFaultMgr: ipc::registerEnablingCondition() + FaultLib <-- DiagFaultMgr: ipc::register(Ok, id) + App <-- FaultLib : Ok(Some) +end + +== Reporting Condition Change == + +App -> EnablingCondition : report_status(active/passive) +EnablingCondition -> FaultLib : report_status() +App <-- EnablingCondition +FaultLib -> DiagFaultMgr : ipc::reportEnablingCondition(id, pass/fail) + + + +@enduml \ No newline at end of file diff --git a/docs/puml/new_enable_condition.svg b/docs/puml/new_enable_condition.svg new file mode 100644 index 0000000..7532f71 --- /dev/null +++ b/docs/puml/new_enable_condition.svg @@ -0,0 +1 @@ +Registering new enable condition by the providerRegistering new enable condition by the providerAppDFMAppEnablingConditionFaultLibDiagFaultMgrAppAppEnablingConditionFaultLibFaultLibDiagFaultMgrDiagFaultMgrStartupipc::subscribeEnablingConditions()ipc::ntfEnablingConditions(list[])get_enabling_condition(sovd_entity)check_if_exist(sovd_entity)altEnablingCondition already exist or errorErrornew::EnablingCondition()EnablingConditionipc::registerEnablingCondition()ipc::register(Ok, id)Ok(Some<EnablingCondition>)Reporting Condition Changereport_status(active/passive)report_status()ipc::reportEnablingCondition(id, pass/fail) \ No newline at end of file diff --git a/docs/puml/new_fault.puml b/docs/puml/new_fault.puml new file mode 100644 index 0000000..d5da0b4 --- /dev/null +++ b/docs/puml/new_fault.puml @@ -0,0 +1,74 @@ +@startuml +title Reporting fault by the app +skinparam ParticipantPadding 20 + +box "App" +participant App +participant FaultMonitor +participant FaultLib +end box +box "DFM" +participant DiagFaultMgr +end box + + +ref over FaultLib, DiagFaultMgr: [[fault_catalog.svg Fault Catalog Init]] + +== Start up == + +FaultLib -> DiagFaultMgr : ipc::subscribeEnablingConditions() +FaultLib <- DiagFaultMgr : ipc::ntfEnablingConditions(list[]) + +App -> FaultLib : get_fault(sovd_path,id,debouncing,enabling_conditions[]) +note right of FaultLib + "This diagram does not consider the case + when the fault doesn't exist in catalog" +end note + +FaultLib -> FaultLib : check_register_fault(sovd_path,enabling_conditions[]) + +alt [[ "Fault doesn't exist in catalog" ]] + + App <-- FaultLib : None +else + + App <-- FaultLib : Some + +create FaultMonitor +App -> FaultMonitor : FaultMonitor::new(Fault) + + +FaultMonitor -> FaultLib : register() +FaultLib -> FaultLib : store(fault_monitor) +App <-- FaultMonitor : +App -> App : store/use FaultMonitor +end +== App Reporting Fault == + +App -> FaultMonitor : report_status(pass/fail) +FaultMonitor -> FaultMonitor : check_debouncing() + +alt ["Debouncing Not passed or enabling conditions not fulfilled"] + App <-- FaultMonitor +else + FaultMonitor -> FaultLib : report(fault_id, status) + App <-- FaultMonitor + FaultLib -> DiagFaultMgr : ipc::report_fault_status(id, status) + alt [["Reporting OK"]] + FaultLib <-- DiagFaultMgr : Ok() + FaultMonitor <-- FaultLib : report(ok) + note right of FaultMonitor + Mark that the last fault report is ok + end note + else + FaultLib <-- DiagFaultMgr : Error() + FaultMonitor <-- FaultLib : report(Error) + note right of FaultMonitor + Depending on the Fault properties, the fault monitor + will decide if make retry or immediately stop + the application (safety case ?) + end note + end +end + +@enduml \ No newline at end of file diff --git a/docs/puml/new_fault.svg b/docs/puml/new_fault.svg new file mode 100644 index 0000000..ad2dfbe --- /dev/null +++ b/docs/puml/new_fault.svg @@ -0,0 +1 @@ +Reporting fault by the appReporting fault by the appAppDFMAppFaultMonitorFaultLibDiagFaultMgrAppAppFaultMonitorFaultLibFaultLibDiagFaultMgrDiagFaultMgrrefFault Catalog InitStart upipc::subscribeEnablingConditions()ipc::ntfEnablingConditions(list[])get_fault(sovd_path,id,debouncing,enabling_conditions[])"This diagram does not consider the casewhen the fault doesn't exist in catalog"check_register_fault(sovd_path,enabling_conditions[])alt[Fault doesn't exist in catalog]NoneSome<Fault(fault_id)>FaultMonitor::new(Fault)FaultMonitorregister()store(fault_monitor)store/use FaultMonitorApp Reporting Faultreport_status(pass/fail)check_debouncing()altDebouncing Not passed or enabling conditions not fulfilledreport(fault_id, status)ipc::report_fault_status(id, status)alt[Reporting OK]Ok()report(ok)Mark that the last fault report is okError()report(Error)Depending on the Fault properties, the fault monitorwill decide if make retry or immediately stopthe application (safety case ?) \ No newline at end of file diff --git a/examples/BUILD b/examples/BUILD new file mode 100644 index 0000000..012dd54 --- /dev/null +++ b/examples/BUILD @@ -0,0 +1,8 @@ +# Needed for Dash tool to check python dependency licenses. +filegroup( + name = "cargo_lock", + srcs = [ + "Cargo.lock", + ], + visibility = ["//visibility:public"], +) diff --git a/patches/BUILD b/patches/BUILD new file mode 100644 index 0000000..62419f4 --- /dev/null +++ b/patches/BUILD @@ -0,0 +1,5 @@ +package(default_visibility = ["//visibility:public"]) + +exports_files([ + "iceoryx2_bb_derive_macros_readme.patch", +]) diff --git a/patches/iceoryx2_bb_derive_macros_readme.patch b/patches/iceoryx2_bb_derive_macros_readme.patch new file mode 100644 index 0000000..adf8978 --- /dev/null +++ b/patches/iceoryx2_bb_derive_macros_readme.patch @@ -0,0 +1,20 @@ +diff --git Cargo.toml Cargo.toml +index 8fc1a73..a7cbab9 100644 +--- Cargo.toml ++++ Cargo.toml +@@ -7,6 +7,6 @@ keywords = { workspace = true } +-license = { workspace = true } +-readme = { workspace = true } ++license = { workspace = true } ++readme = "README.md" + repository = { workspace = true } + rust-version = { workspace = true } + version = { workspace = true } + +diff --git README.md README.md +new file mode 100644 +index 0000000..d969f9d +--- /dev/null ++++ README.md +@@ -0,0 +1 @@ ++Dummy redme for CARGO_PKG_README error. \ No newline at end of file diff --git a/project_config.bzl b/project_config.bzl new file mode 100644 index 0000000..f764a1d --- /dev/null +++ b/project_config.bzl @@ -0,0 +1,5 @@ +# project_config.bzl +PROJECT_CONFIG = { + "asil_level": "QM", + "source_code": ["rust"], +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..78d74c8 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,6 @@ +# rust formatter rules. +# check configuration fields here: https://rust-lang.github.io/rustfmt/?version=v1.6.0&search= + + +tab_spaces = 4 +max_width = 150 \ No newline at end of file diff --git a/src/BUILD b/src/BUILD new file mode 100644 index 0000000..e69de29 diff --git a/src/api.rs b/src/api.rs deleted file mode 100644 index 0211621..0000000 --- a/src/api.rs +++ /dev/null @@ -1,118 +0,0 @@ -/* -* Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) -* -* 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 -*/ - -use crate::{ - catalog::FaultCatalog, - config::ReporterConfig, - ids::FaultId, - model::{FaultDescriptor, FaultLifecycleStage, FaultRecord}, - sink::{FaultSink, LogHook, SinkError}, -}; -use std::{sync::{Arc, OnceLock}, time::SystemTime}; - -// FaultApi acts as a singleton faΓ§ade. A component initializes it once and -// subsequent publishing paths retrieve the sink/logger via global accessors. -pub struct FaultApi; - -static SINK: OnceLock> = OnceLock::new(); -static LOGGER: OnceLock> = OnceLock::new(); - -impl FaultApi { - /// Initialize the singleton. Safe to call once; subsequent calls are ignored. - pub fn new(sink: Arc, logger: Arc) -> Self { - let _ = SINK.set(Arc::clone(&sink)); - let _ = LOGGER.set(Arc::clone(&logger)); - FaultApi - } - - pub(crate) fn get_sink() -> Arc { - SINK.get() - .cloned() - .expect("Sink not initialized - call FaultApi::new() before creating reporters") - } - - pub(crate) fn get_logger() -> Arc { - LOGGER.get() - .cloned() - .expect("Logger not initialized - call FaultApi::new() before creating reporters") - } - - /// Publish a record: log locally then enqueue via sink. Non-blocking semantics depend on sink impl. - pub fn publish(record: &FaultRecord) -> Result<(), SinkError> { - FaultApi::get_logger().on_report(record); - FaultApi::get_sink().publish(record) - } -} - -/// Per-fault reporter bound to a specific fault descriptor. -/// Create one instance per fault at startup. -#[derive(Clone)] -pub struct Reporter { - fault_id: FaultId, - descriptor: FaultDescriptor, - cfg: ReporterConfig, -} - -impl Reporter { - /// Create a new Reporter bound to a specific fault ID. - /// This should be called once per fault during initialization. - pub fn new( - catalog: &FaultCatalog, - cfg: ReporterConfig, - fault_id: &FaultId, - ) -> Self { - let descriptor = catalog - .find(fault_id) - .expect("fault ID must exist in catalog") - .clone(); - - Self { fault_id: fault_id.clone(), descriptor, cfg } - } - - /// Create a new fault record for this specific fault. - /// The returned record can be mutated before publishing. - pub fn create_record(&self) -> FaultRecord { - FaultRecord { - fault_id: self.fault_id.clone(), - time: SystemTime::now(), - severity: self.descriptor.default_severity, - source: self.cfg.source.clone(), - lifecycle_phase: self.cfg.lifecycle_phase, - stage: FaultLifecycleStage::NotTested, - environment_data: self.cfg.default_environment_data.clone(), - } - } - - /// Publish a fault record. Always logs via LogHook, then publishes via sink. - pub fn publish(&self, record: &FaultRecord) -> Result<(), crate::sink::SinkError> { - debug_assert_eq!( - &record.fault_id, &self.fault_id, - "FaultRecord fault_id doesn't match Reporter" - ); - FaultApi::publish(record) - } - - /// Convenience: create and return a record with Failed stage (confirmed failure) - pub fn fail(&self) -> FaultRecord { - let mut rec = self.create_record(); - rec.update_stage(FaultLifecycleStage::Failed); - rec - } - - /// Convenience: create and return a record with Passed stage (healthy) - pub fn pass(&self) -> FaultRecord { - let mut rec = self.create_record(); - rec.update_stage(FaultLifecycleStage::Passed); - rec - } -} diff --git a/src/catalog.rs b/src/catalog.rs deleted file mode 100644 index 74d576e..0000000 --- a/src/catalog.rs +++ /dev/null @@ -1,65 +0,0 @@ -/* -* Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) -* -* 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 -*/ - -use crate::{ids::FaultId, model::FaultDescriptor}; -use std::borrow::Cow; - -/// Declarative catalog shared between reporters and the Diagnostic Fault Manager. -#[derive(Clone, Debug)] -pub struct FaultCatalog { - pub id: Cow<'static, str>, - pub version: u64, - pub descriptors: Cow<'static, [FaultDescriptor]>, -} - -impl FaultCatalog { - pub const fn new( - id: &'static str, - version: u64, - descriptors: &'static [FaultDescriptor], - ) -> Self { - Self { - id: Cow::Borrowed(id), - version, - descriptors: Cow::Borrowed(descriptors), - } - } - - /// When the DFM deserializes a JSON/YAML catalog at startup, this helper - /// lets it hand the owned data back to the library without rebuilding. - pub fn from_config( - id: impl Into>, - version: u64, - descriptors: Vec, - ) -> Self { - Self { - id: id.into(), - version, - descriptors: Cow::Owned(descriptors), - } - } - - /// Locate a descriptor by its FaultId, handy for tests or build tooling. - pub fn find(&self, id: &FaultId) -> Option<&FaultDescriptor> { - self.descriptors.iter().find(|d| &d.id == id) - } - - /// Number of descriptors in this catalog, useful for build-time validation. - pub fn len(&self) -> usize { - self.descriptors.len() - } - - pub fn is_empty(&self) -> bool { - self.descriptors.is_empty() - } -} diff --git a/src/common/BUILD b/src/common/BUILD new file mode 100644 index 0000000..45ca9da --- /dev/null +++ b/src/common/BUILD @@ -0,0 +1,55 @@ +# ******************************************************************************* +# Copyright (c) 2025 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("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") + +filegroup( + name = "common_srcs", + srcs = glob(["src/**/*.rs"]), +) + +rust_library( + name = "common", + srcs = [":common_srcs"], + crate_name = "common", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "@score_fault_lib_crates//:env_logger", + "@score_fault_lib_crates//:iceoryx2", + "@score_fault_lib_crates//:iceoryx2-bb-container", + "@score_fault_lib_crates//:log", + "@score_fault_lib_crates//:mockall", + "@score_fault_lib_crates//:serde", + "@score_fault_lib_crates//:serde_json", + "@score_fault_lib_crates//:sha2", + "@score_fault_lib_crates//:thiserror", + ], +) + +rust_test( + name = "tests", + srcs = [":common_srcs"], + edition = "2024", + deps = [ + "@score_fault_lib_crates//:env_logger", + "@score_fault_lib_crates//:iceoryx2", + "@score_fault_lib_crates//:iceoryx2-bb-container", + "@score_fault_lib_crates//:log", + "@score_fault_lib_crates//:mockall", + "@score_fault_lib_crates//:serde", + "@score_fault_lib_crates//:serde_json", + "@score_fault_lib_crates//:sha2", + "@score_fault_lib_crates//:thiserror", + ], +) diff --git a/src/common/Cargo.toml b/src/common/Cargo.toml new file mode 100644 index 0000000..eb4ed1a --- /dev/null +++ b/src/common/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "common" +version.workspace = true +edition.workspace = true +readme.workspace = true + +[dependencies] +iceoryx2.workspace = true +iceoryx2-bb-container.workspace = true +serde_json.workspace = true +serde = { workspace = true, features = ["derive"] } +log = { workspace = true, features = ["std"] } +mockall.workspace = true +sha2.workspace = true +thiserror.workspace = true diff --git a/src/common/src/config.rs b/src/common/src/config.rs new file mode 100644 index 0000000..f73a5f0 --- /dev/null +++ b/src/common/src/config.rs @@ -0,0 +1,52 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// +use crate::{debounce::DebouncePolicy, fault::ComplianceVec, fault::FaultSeverity, types::*}; +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +use iceoryx2::prelude::*; +use iceoryx2_bb_container::vector::*; + +// Reset rules define how and when a latched fault can be cleared. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ZeroCopySend)] +#[repr(C)] +pub enum ResetTrigger { + /// Clear on next ignition/power cycle count meeting threshold. + PowerCycles(u32), + /// Clear when condition absent for a duration. + StableFor(Duration), + /// Manual maintenance/tooling only (e.g., regulatory). + ToolOnly, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ZeroCopySend)] +#[repr(C)] +pub struct ResetPolicy { + pub trigger: ResetTrigger, + /// Some regulations require X cycles before clearable from user UI. + pub min_operating_cycles_before_clear: Option, +} + +// Per-report options provided by the call site when a fault is emitted. +#[derive(Debug, Default, Clone, ZeroCopySend)] +#[repr(C)] +pub struct ReportOptions { + /// Override severity (else descriptor.default_severity). + pub severity: Option, + /// Attach extra metadata key-values (free form). + pub metadata: StaticVec<(ShortString, ShortString), 8>, + /// Override policies dynamically (rare, but useful for debug/A-B). + pub debounce: Option, + pub reset: Option, + /// Regulatory/operational flagsβ€”extra tags may be added at report time. + pub extra_compliance: ComplianceVec, +} diff --git a/src/common/src/debounce.rs b/src/common/src/debounce.rs new file mode 100644 index 0000000..739f30b --- /dev/null +++ b/src/common/src/debounce.rs @@ -0,0 +1,247 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// +use iceoryx2::prelude::*; +use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; +use std::time::{Duration, Instant}; + +// Debounce descriptions capture how noisy fault sources should be filtered. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ZeroCopySend)] +#[repr(C)] +pub enum DebounceMode { + /// Require N occurrences within a window to confirm fault Active. + CountWithinWindow { min_count: u32, window: Duration }, + /// Confirm when signal remains bad for duration (e.g., stuck-at). + HoldTime { duration: Duration }, + /// Edge triggered (first occurrence) with cooldown to avoid flapping. + EdgeWithCooldown { cooldown: Duration }, +} + +impl DebounceMode { + pub fn into(self) -> Box { + match self { + DebounceMode::CountWithinWindow { min_count, window } => Box::new(CountWithinWindow::new(min_count, window)), + DebounceMode::HoldTime { duration } => Box::new(HoldTime::new(duration)), + DebounceMode::EdgeWithCooldown { cooldown } => Box::new(EdgeWithCooldown::new(cooldown)), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ZeroCopySend)] +#[repr(C)] +pub struct DebouncePolicy { + pub mode: DebounceMode, + /// Optional suppression of repeats in logging within a time window. + pub log_throttle: Option, +} + +pub trait Debounce { + /// Called on each fault occurrence. Returns true if the event should be reported. + fn on_event(&mut self, now: Instant) -> bool; + + /// Resets the internal state, e.g., after a fault clears. + fn reset(&mut self, now: Instant); +} + +pub struct CountWithinWindow { + min_count: u32, + window: Duration, + occurrences: VecDeque, +} + +impl CountWithinWindow { + pub fn new(min_count: u32, window: Duration) -> Self { + Self { + min_count, + window, + occurrences: VecDeque::new(), + } + } +} + +impl Debounce for CountWithinWindow { + fn on_event(&mut self, now: Instant) -> bool { + while self.occurrences.front().is_some_and(|&ts| now.duration_since(ts) > self.window) { + self.occurrences.pop_front(); + } + self.occurrences.push_back(now); + (self.occurrences.len() as u32) >= self.min_count + } + + fn reset(&mut self, _now: Instant) { + self.occurrences.clear(); + } +} + +pub struct HoldTime { + duration: Duration, + start_time: Option, +} + +impl HoldTime { + pub fn new(duration: Duration) -> Self { + Self { duration, start_time: None } + } +} + +impl Debounce for HoldTime { + fn on_event(&mut self, now: Instant) -> bool { + if self.start_time.is_none() { + self.start_time = Some(now); + return false; + } + now.duration_since(self.start_time.unwrap()) >= self.duration + } + + fn reset(&mut self, _now: Instant) { + self.start_time = None; + } +} + +pub struct EdgeWithCooldown { + cooldown: Duration, + last_report: Option, +} + +impl EdgeWithCooldown { + pub fn new(cooldown: Duration) -> Self { + Self { cooldown, last_report: None } + } +} + +impl Debounce for EdgeWithCooldown { + fn on_event(&mut self, now: Instant) -> bool { + match self.last_report { + Some(last) if now.duration_since(last) < self.cooldown => false, + _ => { + self.last_report = Some(now); + true + } + } + } + + fn reset(&mut self, now: Instant) { + self.last_report = Some(now); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{Duration, Instant}; + + #[test] + fn count_with_window_reports_only_after_min_count_within_window() { + let now = Instant::now(); + let mut d = CountWithinWindow::new(3, Duration::from_secs(5)); + assert!(!d.on_event(now)); + assert!(!d.on_event(now + Duration::from_secs(1))); + assert!(d.on_event(now + Duration::from_secs(2))); + assert!(d.on_event(now + Duration::from_secs(3))); + } + + #[test] + fn count_with_window_drops_old_events_outside_window() { + let mut d = CountWithinWindow::new(3, Duration::from_secs(2)); + let t0 = Instant::now(); + assert!(!d.on_event(t0)); + assert!(!d.on_event(t0 + Duration::from_secs(1))); + assert!(d.on_event(t0 + Duration::from_secs(1))); + assert!(!d.on_event(t0 + Duration::from_secs(4))); + assert_eq!(d.occurrences.len(), 1); + } + + #[test] + fn count_with_window_reset_clears_state() { + let mut d = CountWithinWindow::new(2, Duration::from_secs(3)); + let t0 = Instant::now(); + d.on_event(t0); + d.on_event(t0 + Duration::from_secs(1)); + assert!(d.on_event(t0 + Duration::from_secs(2))); + d.reset(t0 + Duration::from_secs(3)); + assert!(!d.on_event(t0 + Duration::from_secs(4))); + } + + #[test] + fn holdtime_requires_continuous_duration_before_report() { + let mut d = HoldTime::new(Duration::from_secs(5)); + let t0 = Instant::now(); + assert!(!d.on_event(t0)); + assert!(!d.on_event(t0 + Duration::from_secs(3))); + assert!(d.on_event(t0 + Duration::from_secs(6))); + } + + #[test] + fn holdtime_reset_resets_timer() { + let mut d = HoldTime::new(Duration::from_secs(5)); + let t0 = Instant::now(); + d.on_event(t0); + d.on_event(t0 + Duration::from_secs(4)); + d.reset(t0 + Duration::from_secs(5)); + assert!(!d.on_event(t0 + Duration::from_secs(6))); + } + + #[test] + fn edge_with_cooldown_reports_first_then_suppresses_during_cooldown() { + let mut d = EdgeWithCooldown::new(Duration::from_secs(5)); + let t0 = Instant::now(); + assert!(d.on_event(t0)); + assert!(!d.on_event(t0 + Duration::from_secs(2))); + assert!(d.on_event(t0 + Duration::from_secs(6))); + } + + #[test] + fn edge_with_cooldown_reset_forces_new_last_report() { + let mut d = EdgeWithCooldown::new(Duration::from_secs(5)); + let t0 = Instant::now(); + d.on_event(t0); + d.reset(t0 + Duration::from_secs(2)); + assert!(!d.on_event(t0 + Duration::from_secs(4))); + assert!(d.on_event(t0 + Duration::from_secs(8))); + } + + #[test] + fn debounce_mode_creates_proper_implementations() { + let d1 = DebounceMode::CountWithinWindow { + min_count: 2, + window: Duration::from_secs(3), + } + .into(); + let d2 = DebounceMode::HoldTime { + duration: Duration::from_secs(1), + } + .into(); + let d3 = DebounceMode::EdgeWithCooldown { + cooldown: Duration::from_secs(10), + } + .into(); + + let now = Instant::now(); + for mut d in [d1, d2, d3] { + d.on_event(now); + d.reset(now); + } + } + + #[test] + fn debounce_policy_derive_traits_work() { + let p1 = DebouncePolicy { + mode: DebounceMode::HoldTime { + duration: Duration::from_secs(2), + }, + log_throttle: Some(Duration::from_secs(10)), + }; + let p2 = p1.clone(); + assert_eq!(p1, p2); + assert!(format!("{:?}", p1).contains("HoldTime")); + } +} diff --git a/src/common/src/fault.rs b/src/common/src/fault.rs new file mode 100644 index 0000000..5f9b522 --- /dev/null +++ b/src/common/src/fault.rs @@ -0,0 +1,124 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +use crate::ResetPolicy; +use crate::debounce::DebounceMode; +use crate::ids::*; +use crate::types::*; +use iceoryx2::prelude::ZeroCopySend; +use iceoryx2_bb_container::vector::StaticVec; +use serde::{Deserialize, Serialize}; + +pub type ComplianceVec = StaticVec; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, ZeroCopySend)] +#[repr(C)] +pub enum FaultId { + Numeric(u32), // e.g., DTC-like + Text(ShortString), // human-stable symbolic ID + Uuid([u8; 16]), // global uniqueness if needed +} + +/// Canonical fault type buckets used for analytics and tooling. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ZeroCopySend)] +#[repr(C)] +pub enum FaultType { + Hardware, + Software, + Communication, + Configuration, + Timing, + Power, + /// Escape hatch for domain-specific groupings until the enum grows. + Custom(ShortString), +} + +/// Align severities to DLT-like levels, stable for logging & UI filters. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, ZeroCopySend)] +#[repr(C)] +pub enum FaultSeverity { + Trace, + Debug, + Info, + Warn, + Error, + Fatal, +} + +/// Compliance/regulatory tags drive escalation, retention, and workflow. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, ZeroCopySend)] +#[repr(C)] +pub enum ComplianceTag { + EmissionRelevant, + SafetyCritical, + SecurityRelevant, + LegalHold, +} + +/// Lifecycle phase of the reporting component/system (for policy gating). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, ZeroCopySend)] +#[repr(C)] +pub enum LifecyclePhase { + Init, + Running, + Suspend, + Resume, + Shutdown, +} + +/// State of a fault’s lifecycle. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, ZeroCopySend)] +#[repr(C)] +pub enum LifecycleStage { + NotTested, // test not executed yet for this reporting window + PreFailed, // initial failure observed but still within debounce/pending window + Failed, // confirmed failure (debounce satisfied / threshold met) + PrePassed, // transitioning back to healthy; stability window accumulating + Passed, // test executed and passed (healthy condition) +} + +/// Immutable, compile-time describer of a fault type (identity + defaults). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct FaultDescriptor { + pub id: FaultId, + + pub name: ShortString, + pub summary: Option, + + pub category: FaultType, + pub severity: FaultSeverity, + pub compliance: ComplianceVec, + + pub reporter_side_debounce: Option, + pub reporter_side_reset: Option, + pub manager_side_debounce: Option, + pub manager_side_reset: Option, +} + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, ZeroCopySend)] +#[repr(C)] +pub struct IpcTimestamp { + pub seconds_since_epoch: u64, + pub nanoseconds: u32, +} + +/// Concrete record produced on each report() call, also logged. +#[derive(Debug, Clone, ZeroCopySend)] +#[repr(C)] +pub struct FaultRecord { + pub id: FaultId, + pub time: IpcTimestamp, + pub source: SourceId, + pub lifecycle_phase: LifecyclePhase, + pub lifecycle_stage: LifecycleStage, + pub env_data: MetadataVec, +} diff --git a/src/common/src/ids.rs b/src/common/src/ids.rs new file mode 100644 index 0000000..9b1db33 --- /dev/null +++ b/src/common/src/ids.rs @@ -0,0 +1,38 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// +use crate::types::*; +use iceoryx2::prelude::ZeroCopySend; +use std::fmt; + +// Lightweight identifiers that keep fault attribution consistent across the fleet. + +#[derive(Debug, Clone, PartialEq, Eq, Hash, ZeroCopySend)] +#[repr(C)] +pub struct SourceId { + pub entity: ShortString, // e.g., "ADAS.Perception", "HVAC" + pub ecu: Option, // e.g., "ECU-A" + pub domain: Option, // e.g., "ADAS", "IVI" + pub sw_component: Option, + pub instance: Option, // allow N instances +} + +const DEFAULT_TAG: ShortString = ShortString::new(); + +impl fmt::Display for SourceId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let ecu = self.ecu.unwrap_or(DEFAULT_TAG); + let dom = self.domain.unwrap_or(DEFAULT_TAG); + let comp = self.sw_component.unwrap_or(DEFAULT_TAG); + let inst = self.instance.unwrap_or(DEFAULT_TAG); + write!(f, "{}@ecu:{} dom:{} comp:{} inst:{}", self.entity, ecu, dom, comp, inst) + } +} diff --git a/src/common/src/ipc_service_name.rs b/src/common/src/ipc_service_name.rs new file mode 100644 index 0000000..04d5fd7 --- /dev/null +++ b/src/common/src/ipc_service_name.rs @@ -0,0 +1,13 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// +pub const DIAGNOSTIC_FAULT_MANAGER_EVENT_SERVICE_NAME: &str = "dfm/event"; +pub const DIAGNOSTIC_FAULT_MANAGER_HASH_CHECK_RESPONSE_SERVICE_NAME: &str = "dfm/event/hash/response"; diff --git a/src/common/src/ipc_service_type.rs b/src/common/src/ipc_service_type.rs new file mode 100644 index 0000000..f3740c9 --- /dev/null +++ b/src/common/src/ipc_service_type.rs @@ -0,0 +1,14 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// +use iceoryx2::prelude::*; + +pub type ServiceType = ipc_threadsafe::Service; diff --git a/src/common/src/lib.rs b/src/common/src/lib.rs new file mode 100644 index 0000000..b023821 --- /dev/null +++ b/src/common/src/lib.rs @@ -0,0 +1,23 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// +pub mod config; +pub mod debounce; +pub mod fault; +pub mod ids; +pub mod ipc_service_name; +pub mod ipc_service_type; +pub mod sink_error; +pub mod types; + +pub use config::{ReportOptions, ResetPolicy}; +pub use fault::FaultId; +pub use ids::SourceId; diff --git a/src/common/src/sink_error.rs b/src/common/src/sink_error.rs new file mode 100644 index 0000000..0cc84a2 --- /dev/null +++ b/src/common/src/sink_error.rs @@ -0,0 +1,29 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +#[derive(thiserror::Error, Debug, PartialEq, Eq, Copy, Clone)] +pub enum SinkError { + #[error("transport unavailable")] + TransportDown, + #[error("rate limited")] + RateLimited, + #[error("permission denied")] + PermissionDenied, + #[error("invalid descriptor: {0}")] + BadDescriptor(&'static str), + #[error("other: {0}")] + Other(&'static str), + #[error("Invalid service name")] + InvalidServiceName, + #[error("Timeout")] + Timeout, +} diff --git a/src/common/src/types.rs b/src/common/src/types.rs new file mode 100644 index 0000000..0854390 --- /dev/null +++ b/src/common/src/types.rs @@ -0,0 +1,28 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +use crate::fault::FaultRecord; +use iceoryx2::prelude::ZeroCopySend; +use iceoryx2_bb_container::{string::StaticString, vector::StaticVec}; + +pub type ShortString = StaticString<64>; +pub type LongString = StaticString<128>; +pub type MetadataVec = StaticVec<(ShortString, ShortString), 8>; +pub type Sha256Vec = StaticVec; + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone, ZeroCopySend)] +#[repr(C)] +pub enum DiagnosticEvent { + Hash((LongString, Sha256Vec)), + Fault((LongString, FaultRecord)), +} diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index a27173f..0000000 --- a/src/config.rs +++ /dev/null @@ -1,95 +0,0 @@ -/* -* Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) -* -* 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 -*/ - -use std::time::Duration; - -// Debounce descriptions capture how noisy fault sources should be filtered. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DebounceMode { - /// Require N occurrences within a window to confirm fault Failed (transition PreFailed -> Failed). - CountWithinWindow { min_count: u32, window: Duration }, - /// Confirm when condition remains continuously bad for at least `duration`. - /// Use for stuck-at / persistent faults where transient glitches should be ignored. - /// Example: sensor delivers identical reading for 60s -> `HoldTime { duration: Duration::from_secs(60) }`. - HoldTime { duration: Duration }, - /// Trigger immediately on first occurrence, then suppress further activations until the cooldown elapses. - /// Use for faults that are meaningful on first edge but may flap rapidly. - /// Example: first CAN bus-off event activates fault, ignore subsequent bus-off transitions for 5s -> `EdgeWithCooldown { cooldown: Duration::from_secs(5) }`. - EdgeWithCooldown { cooldown: Duration }, - /// Pure count based: confirm after total (cumulative) occurrences reach threshold. - /// Useful for sporadic errors where temporal proximity is less important than frequency. - /// Example: activate after 10 checksum mismatches regardless of timing. - CountThreshold { min_count: u32 }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DebouncePolicy { - pub mode: DebounceMode, - /// Optional suppression of repeats in logging within a time window. - pub log_throttle: Option, -} - -// Reset rules define how and when a confirmed (Failed) test result transitions back to Passed. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ResetTrigger { - /// Clear when a given operation cycle kind count meets threshold (e.g. ignition, drive, charge). - /// `cycle_ref` is a symbolic identifier (e.g. "ignition.main", "drive.standard") allowing - /// the DFM to correlate with its cycle counter source. - OperationCycles { kind: OperationCycleKind, min_cycles: u32, cycle_ref: &'static str }, - /// Clear after the fault condition has been continuously absent (tests passing) for `duration`. - /// Relation to cycles: If the reset must align to authoritative operation cycle boundaries, choose - /// `OperationCycles`; `StableFor` is wall/time-source based (monotonic) and independent of cycle counting. - StableFor(Duration), - /// Manual maintenance/tooling only (e.g., regulatory). - DiagnosticTester, -} - -/// Enumerates common operation cycle archetypes relevant for aging/reset semantics. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum OperationCycleKind { - Ignition, // Traditional ignition/power cycle - Drive, // Complete drive cycle (start -> run -> stop) - Charge, // Entire HV battery charge session - Thermal, // HVAC or thermal management cycle - Custom(&'static str), // Domain specific cycle identifier -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ResetPolicy { - pub trigger: ResetTrigger, - /// Some regulations require X cycles before clearable from user UI. - pub min_operating_cycles_before_clear: Option, -} - -// Per-component defaults that get baked into a Reporter instance. -#[derive(Debug, Clone)] -pub struct ReporterConfig { - pub source: crate::ids::SourceId, - pub lifecycle_phase: crate::model::LifecyclePhase, - /// Optional per-reporter defaults (e.g., common metadata). - pub default_environment_data: Vec, -} - -// Per-report options provided by the call site when a fault is emitted. -#[derive(Debug, Clone, Default)] -pub struct ReportOptions { - /// Override severity (else descriptor.default_severity). - pub severity: Option, - /// Attach extra metadata key-values (free form). - pub environment_data: Vec, - /// Override policies dynamically (rare, but useful for debug/A-B). - pub debounce: Option, - pub reset: Option, - /// Regulatory/operational flagsβ€”extra tags may be added at report time. - pub extra_compliance: Vec, -} diff --git a/src/dfm_lib/BUILD b/src/dfm_lib/BUILD new file mode 100644 index 0000000..1ddca53 --- /dev/null +++ b/src/dfm_lib/BUILD @@ -0,0 +1,59 @@ +# ******************************************************************************* +# Copyright (c) 2025 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("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") + +filegroup( + name = "dfm_lib_srcs", + srcs = glob(["src/**/*.rs"]), +) + +rust_library( + name = "dfm_lib", + srcs = [":dfm_lib_srcs"], + crate_name = "dfm_lib", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//src/common", + "@score_fault_lib_crates//:iceoryx2", + "@score_fault_lib_crates//:iceoryx2-bb-container", + "@score_fault_lib_crates//:log", + "@score_fault_lib_crates//:mockall", + "@score_fault_lib_crates//:thiserror", + ], +) + +rust_test( + name = "tests", + srcs = [":dfm_lib_srcs"], + edition = "2024", + deps = [ + "//src/common", + "@score_fault_lib_crates//:iceoryx2", + "@score_fault_lib_crates//:iceoryx2-bb-container", + "@score_fault_lib_crates//:log", + "@score_fault_lib_crates//:mockall", + "@score_fault_lib_crates//:thiserror", + ], +) + +rust_binary( + name = "dfm", + srcs = ["examples/dfm.rs"], + edition = "2024", + deps = [ + ":dfm_lib", + "//src/common", + ], +) diff --git a/src/dfm_lib/Cargo.toml b/src/dfm_lib/Cargo.toml new file mode 100644 index 0000000..9fc00f1 --- /dev/null +++ b/src/dfm_lib/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "dfm_lib" +version.workspace = true +edition.workspace = true +license-file.workspace = true +authors.workspace = true + +[[bin]] +path = "examples/dfm.rs" +name = "dfm" + +[[bin]] +path = "examples/sovd_fault_manager.rs" +name = "sovd_fm" + + +[dependencies] +env_logger.workspace = true +common = { path = "../common" } +fault_lib = { path = "../fault_lib" } +iceoryx2.workspace = true +iceoryx2-bb-container.workspace = true +thiserror.workspace = true +log = { workspace = true, features = ["std"] } +mockall.workspace = true +rust_kvs = { git = "https://github.com/eclipse-score/persistency.git" , branch = "main"} +tempfile = "3.20" +serde_json = "1.0" + +[dev-dependencies] +tempfile = "3.20" +serde_json = "1.0" diff --git a/src/dfm_lib/examples/dfm.rs b/src/dfm_lib/examples/dfm.rs new file mode 100644 index 0000000..2d64f86 --- /dev/null +++ b/src/dfm_lib/examples/dfm.rs @@ -0,0 +1,184 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +use common::debounce; +use common::fault; +use dfm_lib::diagnostic_fault_manager::DiagnosticFaultManager; +use dfm_lib::fault_catalog_registry::*; +use dfm_lib::sovd_fault_manager::*; +use dfm_lib::sovd_fault_storage::*; +use env_logger::Env; +use fault_lib::catalog::{FaultCatalogBuilder, FaultCatalogConfig}; +use fault_lib::utils::to_static_long_string; +use fault_lib::utils::to_static_short_string; +use std::time::Duration; +use tempfile::tempdir; + +fn load_hvac_config() -> FaultCatalogConfig { + let f1 = fault::FaultDescriptor { + id: fault::FaultId::Numeric(0x7001), + + name: to_static_short_string("CabinTempSensorStuck").unwrap(), + summary: None, + + category: fault::FaultType::Communication, + severity: fault::FaultSeverity::Error, + compliance: fault::ComplianceVec::try_from(&[fault::ComplianceTag::EmissionRelevant][..]).unwrap(), + + reporter_side_debounce: Some(debounce::DebounceMode::HoldTime { + duration: Duration::from_secs(60), + }), + reporter_side_reset: None, + manager_side_debounce: None, + manager_side_reset: None, + }; + + let f2 = fault::FaultDescriptor { + id: fault::FaultId::Text(to_static_short_string("hvac.blower.speed_sensor_mismatch").unwrap()), + + name: to_static_short_string("BlowerSpeedMismatch").unwrap(), + summary: Some(to_static_long_string("Human-readable summary").unwrap()), + + category: fault::FaultType::Communication, + severity: fault::FaultSeverity::Error, + compliance: fault::ComplianceVec::try_from(&[fault::ComplianceTag::SecurityRelevant, fault::ComplianceTag::SafetyCritical][..]).unwrap(), + + reporter_side_debounce: None, + reporter_side_reset: None, + manager_side_debounce: Some(debounce::DebounceMode::EdgeWithCooldown { + cooldown: Duration::from_millis(100_u64), + }), + manager_side_reset: None, + }; + + let faults = vec![f1, f2]; + FaultCatalogConfig { + id: "hvac".into(), + version: 3, + faults, + } + + // serde_json::to_string(&[d1, d2]).expect("serde_json::to_string failed") +} + +fn load_ivi_config() -> FaultCatalogConfig { + let f1 = fault::FaultDescriptor { + id: fault::FaultId::Text(to_static_short_string("d1").unwrap()), + + name: to_static_short_string("Descriptor 1").unwrap(), + summary: None, + + category: fault::FaultType::Software, + severity: fault::FaultSeverity::Debug, + compliance: fault::ComplianceVec::try_from(&[fault::ComplianceTag::EmissionRelevant, fault::ComplianceTag::SafetyCritical][..]).unwrap(), + + reporter_side_debounce: Some(debounce::DebounceMode::EdgeWithCooldown { + cooldown: Duration::from_millis(100_u64), + }), + reporter_side_reset: None, + manager_side_debounce: None, + manager_side_reset: None, + }; + + let f2 = fault::FaultDescriptor { + id: fault::FaultId::Text(to_static_short_string("d2").unwrap()), + + name: to_static_short_string("Descriptor 2").unwrap(), + summary: Some(to_static_long_string("Human-readable summary").unwrap()), + + category: fault::FaultType::Configuration, + severity: fault::FaultSeverity::Warn, + compliance: fault::ComplianceVec::try_from(&[fault::ComplianceTag::SecurityRelevant, fault::ComplianceTag::SafetyCritical][..]).unwrap(), + + reporter_side_debounce: None, + reporter_side_reset: None, + manager_side_debounce: Some(debounce::DebounceMode::EdgeWithCooldown { + cooldown: Duration::from_millis(100_u64), + }), + manager_side_reset: None, + }; + + let faults = vec![f1, f2]; + FaultCatalogConfig { + id: "ivi".into(), + version: 1, + faults, + } + + // serde_json::to_string(&[d1, d2]).expect("serde_json::to_string failed") +} +fn main() { + let env = Env::default().filter_or("RUST_LOG", "debug"); + env_logger::init_from_env(env); + + let storage_dir = tempdir().unwrap(); + let storage = KvsSovdFaultStateStorage::new(storage_dir.path(), 0).unwrap(); + + let hvac_catalog = FaultCatalogBuilder::new().cfg_struct(load_hvac_config()).build(); + let ivi_catalog = FaultCatalogBuilder::new().cfg_struct(load_ivi_config()).build(); + + let registry = FaultCatalogRegistry::new(vec![hvac_catalog, ivi_catalog]); + + let dfm = DiagnosticFaultManager::new(storage, registry); + let manager = dfm.get_sovd_fault_manager(); + + // Try to get faults for a non-existent path. + let faults = manager.get_all_faults("invalid_hvac"); + assert!(faults.is_err()); + assert_eq!(faults.unwrap_err(), Error::BadArgument); + + let faults = manager.get_all_faults("hvac").unwrap(); + println!("{:?}", faults); + + /* + let record = fault::FaultRecord { + id: fault::FaultId::Text(to_static_short_string("d1").unwrap()), + time: IpcTimestamp::default(), + source: SourceId { + entity: to_static_short_string("source").unwrap(), + ecu: Some(ShortString::from_bytes("ECU-A".as_bytes()).unwrap()), + domain: Some(to_static_short_string("ADAS").unwrap()), + sw_component: Some(to_static_short_string("Perception").unwrap()), + instance: Some(to_static_short_string("0").unwrap()), + }, + lifecycle_phase: fault::LifecyclePhase::Running, + lifecycle_stage: fault::LifecycleStage::Failed, + env_data: MetadataVec::try_from( + &[ + (to_static_short_string("k1").unwrap(), to_static_short_string("v1").unwrap()), + (to_static_short_string("k2").unwrap(), to_static_short_string("v2").unwrap()), + ][..], + ) + .unwrap(), + }; + + // TODO: Send record via app here + */ + + let faults = manager.get_all_faults("hvac").unwrap(); + println!("{:?}", faults); + + let fault = manager.get_fault("hvac", &faults[0].code).unwrap(); + println!("{:?}", fault); +} diff --git a/src/dfm_lib/examples/sovd_fault_manager.rs b/src/dfm_lib/examples/sovd_fault_manager.rs new file mode 100644 index 0000000..746d326 --- /dev/null +++ b/src/dfm_lib/examples/sovd_fault_manager.rs @@ -0,0 +1,122 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +use common::SourceId; +use common::debounce; +use common::fault; +use common::fault::IpcTimestamp; +use common::types::MetadataVec; +use common::types::ShortString; +use dfm_lib::fault_catalog_registry::*; +use dfm_lib::fault_record_processor::FaultRecordProcessor; +use dfm_lib::sovd_fault_manager::*; +use dfm_lib::sovd_fault_storage::*; +use fault_lib::catalog::{FaultCatalogBuilder, FaultCatalogConfig}; +use fault_lib::utils::to_static_long_string; +use fault_lib::utils::to_static_short_string; +use std::sync::Arc; +use std::time::Duration; +use tempfile::tempdir; + +fn load_config_file() -> FaultCatalogConfig { + let d1 = fault::FaultDescriptor { + id: fault::FaultId::Text(to_static_short_string("d1").unwrap()), + + name: to_static_short_string("Descriptor 1").unwrap(), + summary: None, + + category: fault::FaultType::Software, + severity: fault::FaultSeverity::Debug, + compliance: fault::ComplianceVec::try_from(&[fault::ComplianceTag::EmissionRelevant, fault::ComplianceTag::SafetyCritical][..]).unwrap(), + + reporter_side_debounce: Some(debounce::DebounceMode::EdgeWithCooldown { + cooldown: Duration::from_millis(100_u64), + }), + reporter_side_reset: None, + manager_side_debounce: None, + manager_side_reset: None, + }; + + let d2 = fault::FaultDescriptor { + id: fault::FaultId::Text(to_static_short_string("d2").unwrap()), + + name: to_static_short_string("Descriptor 2").unwrap(), + summary: Some(to_static_long_string("Human-readable summary").unwrap()), + + category: fault::FaultType::Configuration, + severity: fault::FaultSeverity::Warn, + compliance: fault::ComplianceVec::try_from(&[fault::ComplianceTag::SecurityRelevant, fault::ComplianceTag::SafetyCritical][..]).unwrap(), + + reporter_side_debounce: None, + reporter_side_reset: None, + manager_side_debounce: Some(debounce::DebounceMode::EdgeWithCooldown { + cooldown: Duration::from_millis(100_u64), + }), + manager_side_reset: None, + }; + let faults = vec![d1, d2]; + FaultCatalogConfig { + id: "hvac".into(), + version: 3, + faults, + } + // serde_json::to_string(&[d1, d2]).expect("serde_json::to_string failed") +} + +fn main() { + let storage_dir = tempdir().unwrap(); + + let storage = Arc::new(KvsSovdFaultStateStorage::new(storage_dir.path(), 0).unwrap()); + + let cfg = load_config_file(); + let registry = Arc::new(FaultCatalogRegistry::new(vec![FaultCatalogBuilder::new().cfg_struct(cfg).build()])); + + let mut processor = FaultRecordProcessor::new(Arc::clone(&storage), Arc::clone(®istry)); + let manager = SovdFaultManager::new(storage, registry); + + // Try to get faults for a non-existent path. + let faults = manager.get_all_faults("invalid_hvac"); + assert!(faults.is_err()); + assert_eq!(faults.unwrap_err(), Error::BadArgument); + + let faults = manager.get_all_faults("hvac").unwrap(); + println!("{:?}", faults); + + let record = fault::FaultRecord { + id: fault::FaultId::Text(to_static_short_string("d1").unwrap()), + time: IpcTimestamp::default(), + source: SourceId { + entity: to_static_short_string("source").unwrap(), + ecu: Some(ShortString::from_bytes("ECU-A".as_bytes()).unwrap()), + domain: Some(to_static_short_string("ADAS").unwrap()), + sw_component: Some(to_static_short_string("Perception").unwrap()), + instance: Some(to_static_short_string("0").unwrap()), + }, + lifecycle_phase: fault::LifecyclePhase::Running, + lifecycle_stage: fault::LifecycleStage::Failed, + env_data: MetadataVec::try_from( + &[ + (to_static_short_string("k1").unwrap(), to_static_short_string("v1").unwrap()), + (to_static_short_string("k2").unwrap(), to_static_short_string("v2").unwrap()), + ][..], + ) + .unwrap(), + }; + + processor.process_record(&to_static_long_string("hvac").unwrap(), &record); + + let faults = manager.get_all_faults("hvac").unwrap(); + println!("{:?}", faults); + + let fault = manager.get_fault("hvac", &faults[0].code).unwrap(); + println!("{:?}", fault); +} diff --git a/src/dfm_lib/src/diagnostic_fault_manager.rs b/src/dfm_lib/src/diagnostic_fault_manager.rs new file mode 100644 index 0000000..d5ad958 --- /dev/null +++ b/src/dfm_lib/src/diagnostic_fault_manager.rs @@ -0,0 +1,65 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +use crate::{ + fault_catalog_registry::FaultCatalogRegistry, fault_lib_communicator::FaultLibCommunicator, fault_record_processor::FaultRecordProcessor, + sovd_fault_manager::SovdFaultManager, sovd_fault_storage::SovdFaultStateStorage, +}; +use log::error; +use std::{ + sync::Arc, + thread::{self, JoinHandle}, +}; + +pub struct DiagnosticFaultManager { + fault_lib_receiver_thread: Option>, + // TODO: we will also need the SOVD server connection to be added here and in the builder + storage: Arc, + registry: Arc, +} + +impl DiagnosticFaultManager { + pub fn new(storage: S, registry: FaultCatalogRegistry) -> Self { + let storage = Arc::new(storage); + let registry = Arc::new(registry); + let processor = FaultRecordProcessor::new(Arc::clone(&storage), Arc::clone(®istry)); + + // TODO: Add a SOVD server connection builder and pass the result as parameter + let handle: JoinHandle<()> = thread::Builder::new() + .name("fault_lib_receiver_thread".into()) + .spawn(move || { + FaultLibCommunicator::new().run(processor); + }) + .expect("Failed to spawn the fault_lib_receiver_thread"); + + Self { + fault_lib_receiver_thread: Some(handle), + storage, + registry, + } + } + + pub fn get_sovd_fault_manager(&self) -> SovdFaultManager { + SovdFaultManager::new(Arc::clone(&self.storage), Arc::clone(&self.registry)) + } +} + +impl Drop for DiagnosticFaultManager { + fn drop(&mut self) { + println!("Joining fault_lib_receiver_thread"); + let handle = self.fault_lib_receiver_thread.take().unwrap(); + if let Err(err) = handle.join() { + error!("fault_lib_receiver_thread panicked: {:?}", err); + } + println!("fault_lib_receiver_thread done!"); + } +} diff --git a/src/dfm_lib/src/fault_catalog_registry.rs b/src/dfm_lib/src/fault_catalog_registry.rs new file mode 100644 index 0000000..cae259f --- /dev/null +++ b/src/dfm_lib/src/fault_catalog_registry.rs @@ -0,0 +1,32 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +use fault_lib::catalog::FaultCatalog; +use std::{borrow::Cow, collections::HashMap}; + +//TODO: The registry was needed to implement the SovdFaultManager. This is just a mock, and will be changed. + +pub struct FaultCatalogRegistry { + pub(crate) catalogs: HashMap, FaultCatalog>, +} + +impl FaultCatalogRegistry { + pub fn new(entries: Vec) -> Self { + Self { + catalogs: entries.into_iter().map(|kv| (kv.id.clone(), kv)).collect(), + } + } + + pub fn get(&self, path: &str) -> Option<&FaultCatalog> { + self.catalogs.get(path) + } +} diff --git a/src/dfm_lib/src/fault_lib_communicator.rs b/src/dfm_lib/src/fault_lib_communicator.rs new file mode 100644 index 0000000..968e27c --- /dev/null +++ b/src/dfm_lib/src/fault_lib_communicator.rs @@ -0,0 +1,101 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// +use common::ipc_service_name::{DIAGNOSTIC_FAULT_MANAGER_EVENT_SERVICE_NAME, DIAGNOSTIC_FAULT_MANAGER_HASH_CHECK_RESPONSE_SERVICE_NAME}; +use common::ipc_service_type::ServiceType; +use common::sink_error::SinkError; +use common::types::DiagnosticEvent; + +use crate::fault_record_processor::FaultRecordProcessor; +use crate::sovd_fault_storage::SovdFaultStateStorage; +use iceoryx2::node::NodeBuilder; +use iceoryx2::port::publisher::Publisher; +use iceoryx2::port::subscriber::Subscriber; +use iceoryx2::prelude::{Node, NodeName, ServiceName}; +use log::info; +use std::time::Duration; + +const DIAGNOSTIC_FAULT_MANAGER_LISTENER_NODE_NAME: &str = "fault_listener_node"; +const DIAGNOSTIC_FAULT_MANAGER_LISTENER_CYCLE_TIME: Duration = Duration::from_millis(10); + +pub struct FaultLibCommunicator { + catalog_hash_response_publisher: Publisher, + diagnostic_event_subscriber: Subscriber, + node: Node, +} + +impl FaultLibCommunicator { + pub fn new() -> Self { + let node_name = NodeName::new(DIAGNOSTIC_FAULT_MANAGER_LISTENER_NODE_NAME).unwrap(); + let node = NodeBuilder::new() + .name(&node_name) + .create::() + .expect("Failed to create listener node"); + + let diagnostic_event_subscriber_service_name = ServiceName::new(DIAGNOSTIC_FAULT_MANAGER_EVENT_SERVICE_NAME).unwrap(); + let diagnostic_event_subscriber_service = node + .service_builder(&diagnostic_event_subscriber_service_name) + .publish_subscribe::() + .open_or_create() + .expect("Failed to create the event listener service"); + let diagnostic_event_subscriber = diagnostic_event_subscriber_service + .subscriber_builder() + .create() + .expect("Failed to create subscriber"); + + let hash_response_service_name = + ServiceName::new(DIAGNOSTIC_FAULT_MANAGER_HASH_CHECK_RESPONSE_SERVICE_NAME).expect("Failed to create the fault service name"); + let hash_response_service = node + .service_builder(&hash_response_service_name) + .publish_subscribe::() + .open_or_create() + .expect("Failed to create the hash transmitter service"); + let catalog_hash_response_publisher = hash_response_service + .publisher_builder() + .create() + .expect("Failed to create the hash transmitter client"); + + FaultLibCommunicator { + diagnostic_event_subscriber, + catalog_hash_response_publisher, + node, + } + } + + fn publish_catalog_hash_response(&self, hash_response: bool) -> Result<(), SinkError> { + let sample = self.catalog_hash_response_publisher.loan_uninit().map_err(|_| SinkError::TransportDown)?; + let sample = sample.write_payload(hash_response); + match sample.send().map_err(|_| SinkError::TransportDown) { + Ok(_) => Ok(()), + Err(e) => Err(e), + } + } + + pub fn run(&self, mut processor: FaultRecordProcessor) { + info!("FaultLibCommunicator listening..."); + while self.node.wait(DIAGNOSTIC_FAULT_MANAGER_LISTENER_CYCLE_TIME).is_ok() { + while let Some(sample) = self.diagnostic_event_subscriber.receive().unwrap() { + match sample.payload() { + DiagnosticEvent::Fault((path, fault)) => { + info!("Received new fault ID: {:?}", fault.id); + processor.process_record(path, fault); + } + DiagnosticEvent::Hash((path, hash_sum)) => { + let result = processor.check_hash_sum(path, hash_sum); + info!("Received hash: {:?}", hash_sum); + self.publish_catalog_hash_response(result).unwrap(); + } + } + } + } + info!("FaultLibCommunicator stopped"); + } +} diff --git a/src/dfm_lib/src/fault_record_processor.rs b/src/dfm_lib/src/fault_record_processor.rs new file mode 100644 index 0000000..50b517e --- /dev/null +++ b/src/dfm_lib/src/fault_record_processor.rs @@ -0,0 +1,68 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +use crate::fault_catalog_registry::FaultCatalogRegistry; +use crate::sovd_fault_storage::SovdFaultStateStorage; +use crate::sovd_fault_storage::*; +use common::fault; +use common::types::{LongString, Sha256Vec}; +use log::{error, info}; +use std::sync::Arc; + +pub struct FaultRecordProcessor { + storage: Arc, + catalog_registry: Arc, +} + +impl FaultRecordProcessor { + pub fn new(storage: Arc, catalog_registry: Arc) -> Self { + Self { storage, catalog_registry } + } + + pub fn process_record(&mut self, path: &LongString, record: &fault::FaultRecord) { + let mut state = SovdFaultState::default(); + match record.lifecycle_stage { + fault::LifecycleStage::Failed => { + state.test_failed = true; + state.confirmed_dtc = true; + } + fault::LifecycleStage::Passed => { + state.test_failed = false; + state.confirmed_dtc = false; + } + _ => todo!("Unsupported "), + } + + // TODO: Could consume the record and avoid cloning. + state.env_data = record.env_data.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(); + let storage_result = self.storage.put(&path.to_string(), &record.id, state); + info!("Fault ID {:?} stored : {:?}", record.id, storage_result); + } + + pub fn check_hash_sum(&self, path: &LongString, hash_sum: &Sha256Vec) -> bool { + match self.catalog_registry.get(&path.to_string()) { + Some(catalog) => { + let ret = catalog.config_hash() == hash_sum.to_vec(); + if !ret { + error!("Fault catalog hash sum error for {:?}", path.to_string()); + error!("Expected {:?}", catalog.config_hash()); + error!("Received {:?}", hash_sum.to_vec()); + } + ret + } + None => { + error!("Catalog hash sum entity {:?} not found ", path.to_string()); + false + } + } + } +} diff --git a/src/dfm_lib/src/lib.rs b/src/dfm_lib/src/lib.rs new file mode 100644 index 0000000..a4c702e --- /dev/null +++ b/src/dfm_lib/src/lib.rs @@ -0,0 +1,17 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// +pub mod diagnostic_fault_manager; +pub mod fault_catalog_registry; +pub(crate) mod fault_lib_communicator; +pub mod fault_record_processor; +pub mod sovd_fault_manager; +pub mod sovd_fault_storage; diff --git a/src/dfm_lib/src/sovd_fault_manager.rs b/src/dfm_lib/src/sovd_fault_manager.rs new file mode 100644 index 0000000..42d65ed --- /dev/null +++ b/src/dfm_lib/src/sovd_fault_manager.rs @@ -0,0 +1,131 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +use crate::fault_catalog_registry::FaultCatalogRegistry; +use crate::sovd_fault_storage::*; +use common::{fault, types::ShortString}; +use std::{collections::HashMap, sync::Arc}; + +#[derive(Debug, PartialEq, Eq)] +pub enum Error { + BadArgument, + Generic, +} + +#[derive(Clone, Default, Debug, PartialEq, Eq)] +pub struct SovdFault { + pub code: String, + pub display_code: String, + pub scope: String, + pub fault_name: String, + pub fault_translation_id: String, + pub severity: u32, + pub status: HashMap, +} + +impl SovdFault { + fn new(descriptor: &fault::FaultDescriptor, state: &SovdFaultState) -> Self { + Self { + code: fault_id_to_code(&descriptor.id), + display_code: fault_id_to_code(&descriptor.id), + scope: "".into(), + fault_name: descriptor.name.to_string(), + fault_translation_id: "".into(), + severity: descriptor.severity as u32, + status: HashMap::from([ + ("testFailed".into(), (state.test_failed as u32).to_string()), + ( + "testFailedThisOperationCycle".into(), + (state.test_failed_this_operation_cycle as u32).to_string(), + ), + ("testFailedSinceLastClear".into(), (state.test_failed_since_last_clear as u32).to_string()), + ( + "testNotCompletedThisOperationCycle".into(), + (state.test_not_completed_this_operation_cycle as u32).to_string(), + ), + ( + "testNotCompletedSinceLastClear".into(), + (state.test_not_completed_since_last_clear as u32).to_string(), + ), + ("pendingDTC".into(), (state.pending_dtc as u32).to_string()), + ("confirmedDTC".into(), (state.confirmed_dtc as u32).to_string()), + ("warningIndicatorRequested".into(), (state.warning_indicator_requested as u32).to_string()), + ]), + } + } +} + +pub type SovdEnvData = HashMap; + +pub struct SovdFaultManager { + storage: Arc, + registry: Arc, +} + +impl SovdFaultManager { + pub fn new(storage: Arc, registry: Arc) -> Self { + Self { storage, registry } + } + + pub fn get_all_faults(&self, path: &str) -> Result, Error> { + let Some(catalog) = self.registry.catalogs.get(path) else { + return Err(Error::BadArgument); + }; + let descriptors = catalog.descriptors(); + let mut faults = Vec::new(); + + for descriptor in descriptors { + // Right now all faults are always returned. Faults for which a record wasn't received are returned with a clear status. + // TODO: Need to decide if this is the correct behavior. It could be that a fault shouldn't be reported once Passed. + let state = self.storage.get(path, &descriptor.id).unwrap_or(SovdFaultState::default()); + + faults.push(SovdFault::new(descriptor, &state)); + } + + Ok(faults) + } + + pub fn get_fault(&self, path: &str, fault_code: &str) -> Result<(SovdFault, SovdEnvData), Error> { + let Some(catalog) = self.registry.catalogs.get(path) else { + return Err(Error::BadArgument); + }; + let fault_id = fault_id_from_code(fault_code); + let Some(descriptor) = catalog.descriptor(&fault_id) else { + return Err(Error::Generic); + }; + // Faults for which a record wasn't received are returned with a clear status. + // TODO: Need to decide if this is the correct behavior. It could be that a fault shouldn't be reported once Passed. + let state = self.storage.get(path, &fault_id).unwrap_or_default(); + + Ok((SovdFault::new(descriptor, &state), state.env_data)) + } + + pub fn delete_all_faults(&mut self, _path: &str) -> Result<(), Error> { + todo!("Not implemented"); + } + + pub fn delete_fault(&mut self, _path: &str, _fault_code: &str) -> Result<(), Error> { + todo!("Not implemented"); + } +} + +fn fault_id_to_code(fault_id: &fault::FaultId) -> String { + match fault_id { + fault::FaultId::Numeric(_) => todo!("Unsupported"), + fault::FaultId::Text(t) => t.to_string(), + fault::FaultId::Uuid(_) => todo!("Unsupported"), + } +} + +fn fault_id_from_code(fault_code: &str) -> fault::FaultId { + fault::FaultId::Text(ShortString::try_from(fault_code).expect("Failed to convert fault code to fault id")) +} diff --git a/src/dfm_lib/src/sovd_fault_storage.rs b/src/dfm_lib/src/sovd_fault_storage.rs new file mode 100644 index 0000000..3ff830f --- /dev/null +++ b/src/dfm_lib/src/sovd_fault_storage.rs @@ -0,0 +1,226 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +use crate::sovd_fault_manager::SovdEnvData; +use common::{fault, types::ShortString}; +use rust_kvs::prelude::*; +use serde_json::to_string; +use std::{collections::HashMap, path::Path}; + +// TODO: Most of the storage should be pub(crate). + +#[derive(Default, Debug, PartialEq, Eq)] +pub struct SovdFaultState { + // TODO: Just use an integer for the DTC flags. + pub(crate) test_failed: bool, + pub(crate) test_failed_this_operation_cycle: bool, + pub(crate) test_failed_since_last_clear: bool, + pub(crate) test_not_completed_this_operation_cycle: bool, + pub(crate) test_not_completed_since_last_clear: bool, + pub(crate) pending_dtc: bool, + pub(crate) confirmed_dtc: bool, + pub(crate) warning_indicator_requested: bool, + pub(crate) env_data: SovdEnvData, +} + +impl KvsSerialize for SovdFaultState { + type Error = ErrorCode; + + fn to_kvs(&self) -> Result { + let mut map = KvsMap::new(); + map.insert("test_failed".to_string(), self.test_failed.to_kvs()?); + map.insert( + "test_failed_this_operation_cycle".to_string(), + self.test_failed_this_operation_cycle.to_kvs()?, + ); + map.insert("test_failed_since_last_clear".to_string(), self.test_failed_since_last_clear.to_kvs()?); + map.insert( + "test_not_completed_this_operation_cycle".to_string(), + self.test_not_completed_this_operation_cycle.to_kvs()?, + ); + map.insert( + "test_not_completed_since_last_clear".to_string(), + self.test_not_completed_since_last_clear.to_kvs()?, + ); + map.insert("pending_dtc".to_string(), self.pending_dtc.to_kvs()?); + map.insert("confirmed_dtc".to_string(), self.confirmed_dtc.to_kvs()?); + map.insert("warning_indicator_requested".to_string(), self.warning_indicator_requested.to_kvs()?); + let kvs_env_data = self + .env_data + .iter() + .map(|(k, v)| (k.clone(), KvsValue::from(v.as_str()))) + .collect::(); + map.insert("env_data".to_string(), kvs_env_data.to_kvs()?); + map.to_kvs() + } +} + +impl KvsDeserialize for SovdFaultState { + type Error = ErrorCode; + + fn from_kvs(kvs_value: &KvsValue) -> Result { + if let KvsValue::Object(map) = kvs_value { + let kvs_env_data = KvsMap::from_kvs(map.get("env_data").ok_or(ErrorCode::DeserializationFailed("".to_string()))?)?; + let mut env_data = HashMap::new(); + + for (k, v) in kvs_env_data { + env_data.insert(k, String::from_kvs(&v)?); + } + + Ok(SovdFaultState { + test_failed: bool::from_kvs(map.get("test_failed").ok_or(ErrorCode::DeserializationFailed("".to_string()))?)?, + test_failed_this_operation_cycle: bool::from_kvs( + map.get("test_failed_this_operation_cycle") + .ok_or(ErrorCode::DeserializationFailed("".to_string()))?, + )?, + test_failed_since_last_clear: bool::from_kvs( + map.get("test_failed_since_last_clear") + .ok_or(ErrorCode::DeserializationFailed("".to_string()))?, + )?, + test_not_completed_this_operation_cycle: bool::from_kvs( + map.get("test_not_completed_this_operation_cycle") + .ok_or(ErrorCode::DeserializationFailed("".to_string()))?, + )?, + test_not_completed_since_last_clear: bool::from_kvs( + map.get("test_not_completed_since_last_clear") + .ok_or(ErrorCode::DeserializationFailed("".to_string()))?, + )?, + pending_dtc: bool::from_kvs(map.get("pending_dtc").ok_or(ErrorCode::DeserializationFailed("".to_string()))?)?, + confirmed_dtc: bool::from_kvs(map.get("confirmed_dtc").ok_or(ErrorCode::DeserializationFailed("".to_string()))?)?, + warning_indicator_requested: bool::from_kvs( + map.get("warning_indicator_requested") + .ok_or(ErrorCode::DeserializationFailed("".to_string()))?, + )?, + env_data, + }) + } else { + Err(ErrorCode::DeserializationFailed("".to_string())) + } + } +} + +pub trait SovdFaultStateStorage: Send + Sync { + // TODO: Return Result instead of a bool or Option. + fn put(&self, path: &str, fault_id: &fault::FaultId, state: SovdFaultState) -> bool; + fn get_all(&self, path: &str) -> Option>; + fn get(&self, path: &str, fault_id: &fault::FaultId) -> Option; + fn delete_all(&self, path: &str) -> bool; + fn delete(&self, path: &str, fault_id: &fault::FaultId) -> bool; +} + +pub struct KvsSovdFaultStateStorage { + kvs: Kvs, /* + "path": { + "fault id": FaultStatus, + "fault id": FaultStatus, + ... + }, + "path": { + ... + }, + ... + */ +} + +impl KvsSovdFaultStateStorage { + // TODO: Return Result instead of Optiona. + pub fn new(dir: &Path, instance: usize) -> Option { + let builder = KvsBuilder::new(InstanceId(instance)) + .backend(Box::new(JsonBackendBuilder::new().working_dir(dir.to_path_buf()).build())) + .kvs_load(KvsLoad::Optional); + let kvs = builder.build().expect("Failed to build Kvs"); + + Some(Self { kvs }) + } +} + +impl SovdFaultStateStorage for KvsSovdFaultStateStorage { + fn put(&self, path: &str, fault_id: &fault::FaultId, state: SovdFaultState) -> bool { + let mut states = self.kvs.get_value_as::(path).unwrap_or(HashMap::new()); + states.insert(fault_id_to_key(fault_id), state.to_kvs().expect("Failed to serialize FaultState")); + self.kvs.set_value(path, states).expect("Failed to update FaultState"); + + true + } + + fn get_all(&self, path: &str) -> Option> { + let mut result = Vec::new(); + + if let Ok(states) = self.kvs.get_value_as::(path) { + for (fault_id_key, state) in &states { + result.push(( + fault_id_from_key(fault_id_key), + SovdFaultState::from_kvs(state).expect("Failed to deserialize FaultState"), + )); + } + + return Some(result); + } + + None + } + + fn get(&self, path: &str, fault_id: &fault::FaultId) -> Option { + let states = self.kvs.get_value_as::(path).ok()?; + let state = states.get(&fault_id_to_key(fault_id))?; + Some(SovdFaultState::from_kvs(state).expect("Failed to deserialize FaultState")) + } + + fn delete_all(&self, _path: &str) -> bool { + todo!("Unsupported") + } + + fn delete(&self, _path: &str, _fault_id: &fault::FaultId) -> bool { + todo!("Unsupported") + } +} + +fn fault_id_to_key(fault_id: &fault::FaultId) -> String { + match fault_id { + fault::FaultId::Numeric(x) => to_string(x).expect("Invalid fault ID value"), + fault::FaultId::Text(t) => t.to_string(), + fault::FaultId::Uuid(_) => todo!("Unsupported"), + } +} + +fn fault_id_from_key(key: &str) -> fault::FaultId { + fault::FaultId::Text(ShortString::try_from(key).expect("Failed to convert key to fault id")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn to_and_from_kvs() { + let state = SovdFaultState { + test_failed: true, + test_failed_this_operation_cycle: false, + test_failed_since_last_clear: true, + test_not_completed_this_operation_cycle: true, + test_not_completed_since_last_clear: false, + pending_dtc: false, + confirmed_dtc: false, + warning_indicator_requested: true, + env_data: SovdEnvData::from([ + ("key1".into(), "val1".into()), + ("key2".into(), "val2".into()), + ("key3".into(), "val3".into()), + ]), + }; + + let state_to_kvs = state.to_kvs().unwrap(); + let state_from_kvs = SovdFaultState::from_kvs(&state_to_kvs).unwrap(); + + assert_eq!(state, state_from_kvs); + } +} diff --git a/src/fault_lib/BUILD b/src/fault_lib/BUILD new file mode 100644 index 0000000..9775889 --- /dev/null +++ b/src/fault_lib/BUILD @@ -0,0 +1,91 @@ +# ******************************************************************************* +# Copyright (c) 2025 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("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") + +filegroup( + name = "fault_lib_srcs", + srcs = glob(["src/**/*.rs"]), +) + +rust_library( + name = "fault_lib", + srcs = [":fault_lib_srcs"], + crate_name = "fault_lib", + edition = "2024", + proc_macro_deps = [ + "@score_fault_lib_crates//:async-trait", + ], + visibility = ["//visibility:public"], + deps = [ + "//src/common", + "@score_fault_lib_crates//:env_logger", + "@score_fault_lib_crates//:iceoryx2", + "@score_fault_lib_crates//:iceoryx2-bb-container", + "@score_fault_lib_crates//:log", + "@score_fault_lib_crates//:mockall", + "@score_fault_lib_crates//:serde", + "@score_fault_lib_crates//:serde_json", + "@score_fault_lib_crates//:sha2", + "@score_fault_lib_crates//:thiserror", + ], +) + +rust_test( + name = "tests", + srcs = [":fault_lib_srcs"], + edition = "2024", + deps = [ + "//src/common", + "@score_fault_lib_crates//:env_logger", + "@score_fault_lib_crates//:iceoryx2", + "@score_fault_lib_crates//:iceoryx2-bb-container", + "@score_fault_lib_crates//:log", + "@score_fault_lib_crates//:mockall", + "@score_fault_lib_crates//:serde", + "@score_fault_lib_crates//:serde_json", + "@score_fault_lib_crates//:sha2", + "@score_fault_lib_crates//:thiserror", + ], +) + +rust_binary( + name = "tst_app", + srcs = ["examples/tst_app.rs"], + edition = "2024", + deps = [ + ":fault_lib", + "//src/common", + "@score_fault_lib_crates//:env_logger", + "@score_fault_lib_crates//:iceoryx2", + "@score_fault_lib_crates//:iceoryx2-bb-container", + "@score_fault_lib_crates//:log", + "@score_fault_lib_crates//:clap", + ], +) + +rust_binary( + name = "catalog_and_reporter", + srcs = ["examples/catalog_and_reporter.rs"], + edition = "2024", + deps = [ + ":fault_lib", + "//src/common", + "@score_fault_lib_crates//:env_logger", + "@score_fault_lib_crates//:iceoryx2-bb-container", + "@score_fault_lib_crates//:log", + "@score_fault_lib_crates//:serde", + "@score_fault_lib_crates//:serde_json", + "@score_fault_lib_crates//:sha2", + ], +) diff --git a/src/fault_lib/Cargo.toml b/src/fault_lib/Cargo.toml new file mode 100644 index 0000000..3d7f429 --- /dev/null +++ b/src/fault_lib/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "fault_lib" +version.workspace = true +edition.workspace = true +readme.workspace = true + +[[bin]] +path = "examples/tst_app.rs" +name = "tst_app" + +[dependencies] +common = {path = "../common"} +async-trait = "0.1.89" +env_logger.workspace = true +iceoryx2.workspace = true +iceoryx2-bb-container.workspace = true +log = { workspace = true, features = ["std"] } +mockall.workspace = true +serde_json.workspace = true +serde = { workspace = true, features = ["derive"] } +sha2.workspace = true +thiserror.workspace = true +clap = { version = "4.5.53", features = ["derive"] } diff --git a/src/fault_lib/examples/catalog_and_reporter.rs b/src/fault_lib/examples/catalog_and_reporter.rs new file mode 100644 index 0000000..e459a29 --- /dev/null +++ b/src/fault_lib/examples/catalog_and_reporter.rs @@ -0,0 +1,87 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +use common::{ + SourceId, + fault::{FaultId, LifecyclePhase, LifecycleStage}, + types::*, +}; +use fault_lib::{ + FaultApi, + catalog::FaultCatalogBuilder, + reporter::{Reporter, ReporterApi, ReporterConfig}, + test_utils::*, + utils::*, +}; +use std::thread; + +fn main() { + let json = load_dummy_config_file(); + let _api = FaultApi::new(FaultCatalogBuilder::new().json_string(&json).build()); + + let t1 = thread::spawn(move || { + let source = SourceId { + entity: to_static_short_string("entity1").unwrap(), + ecu: Some(to_static_short_string("ecu").unwrap()), + domain: Some(to_static_short_string("domain").unwrap()), + sw_component: Some(to_static_short_string("component1").unwrap()), + instance: Some(to_static_short_string("1").unwrap()), + }; + let config = ReporterConfig { + source, + lifecycle_phase: LifecyclePhase::Running, + default_env_data: MetadataVec::try_from( + &[ + (to_static_short_string("k1").unwrap(), to_static_short_string("v1").unwrap()), + (to_static_short_string("k2").unwrap(), to_static_short_string("v2").unwrap()), + ][..], + ) + .unwrap(), + }; + + let mut reporter = Reporter::new(&FaultId::Text(to_static_short_string("d1").unwrap()), config).expect("get_descriptor failed"); + + let record = reporter.create_record(LifecycleStage::Passed); + + let _ = reporter.publish("test/path", record); + }); + + let t2 = thread::spawn(move || { + let source = SourceId { + entity: to_static_short_string("entity2").unwrap(), + ecu: Some(to_static_short_string("ecu").unwrap()), + domain: Some(to_static_short_string("domain").unwrap()), + sw_component: Some(to_static_short_string("component2").unwrap()), + instance: Some(to_static_short_string("2").unwrap()), + }; + let config = ReporterConfig { + source, + lifecycle_phase: LifecyclePhase::Running, + default_env_data: MetadataVec::try_from( + &[ + (to_static_short_string("k1").unwrap(), to_static_short_string("v1").unwrap()), + (to_static_short_string("k2").unwrap(), to_static_short_string("v2").unwrap()), + ][..], + ) + .unwrap(), + }; + + let mut reporter = Reporter::new(&FaultId::Text(to_static_short_string("d2").unwrap()), config).expect("get_descriptor failed"); + + let record = reporter.create_record(LifecycleStage::Passed); + + let _ = reporter.publish("test/path", record); + }); + + let _ = t1.join(); + let _ = t2.join(); +} diff --git a/src/fault_lib/examples/config_1.json b/src/fault_lib/examples/config_1.json new file mode 100644 index 0000000..9bc7877 --- /dev/null +++ b/src/fault_lib/examples/config_1.json @@ -0,0 +1,54 @@ +{ + "id": "test_app1", + "version": 1, + "faults": [ + { + "id": { + "Text": "d1" + }, + "name": "Descriptor 1", + "summary": null, + "category": "Software", + "severity": "Debug", + "compliance": [ + "EmissionRelevant", + "SafetyCritical" + ], + "reporter_side_debounce": { + "EdgeWithCooldown": { + "cooldown": { + "secs": 0, + "nanos": 100000000 + } + } + }, + "reporter_side_reset": null, + "manager_side_debounce": null, + "manager_side_reset": null + }, + { + "id": { + "Text": "d2" + }, + "name": "Descriptor 2", + "summary": "Human-readable summary", + "category": "Configuration", + "severity": "Warn", + "compliance": [ + "SecurityRelevant", + "SafetyCritical" + ], + "reporter_side_debounce": null, + "reporter_side_reset": null, + "manager_side_debounce": { + "EdgeWithCooldown": { + "cooldown": { + "secs": 0, + "nanos": 100000000 + } + } + }, + "manager_side_reset": null + } + ] +} \ No newline at end of file diff --git a/src/fault_lib/examples/tst_app.rs b/src/fault_lib/examples/tst_app.rs new file mode 100644 index 0000000..757550a --- /dev/null +++ b/src/fault_lib/examples/tst_app.rs @@ -0,0 +1,74 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// +use clap::Parser; +use common::fault::*; +use env_logger::Env; +use fault_lib::FaultApi; +use fault_lib::catalog::FaultCatalogBuilder; + +use fault_lib::reporter::Reporter; +use fault_lib::reporter::ReporterApi; +use fault_lib::test_utils::*; + +use log::*; +use std::path::PathBuf; +use std::thread; +use std::time::Duration; + +/// Command line arguments +#[derive(Parser, Debug)] +#[command(version, about, long_about= None)] +struct Args { + /// path to fault catalog json file + #[arg(short, long)] + config_file: PathBuf, +} + +fn main() { + let args = Args::parse(); + + let env = Env::default().filter_or("RUST_LOG", "debug"); + env_logger::init_from_env(env); + info!("Start Basic fault library example"); + // Create the FaultLib API object. We have to create it before any Fault API can be used + // and keep it on stack until end of the program. No need to hand it over somewhere + let _api = FaultApi::new(FaultCatalogBuilder::new().json_file(args.config_file).build()); + + // here you can use any public api from fault-api + playground(); + info!("End Basic fault library example"); +} + +fn playground() { + let sovd_path = FaultApi::get_fault_catalog().id.to_string(); + let mut faults = Vec::new(); + + for desc in FaultApi::get_fault_catalog().descriptors() { + faults.push(desc.id.clone()); + } + + let mut reporters = Vec::new(); + + for fault in faults { + reporters.push(Reporter::new(&fault, stub_config()).expect("get_descriptor failed")); + } + + for x in 0..20 { + debug!("Loop {x}"); + + for reporter in reporters.iter_mut() { + let stage = if (x % 2) == 0 { LifecycleStage::Passed } else { LifecycleStage::Failed }; + reporter.publish(&sovd_path, reporter.create_record(stage)).expect("publish failed"); + } + thread::sleep(Duration::from_millis(200)); + } +} diff --git a/src/fault_lib/src/api.rs b/src/fault_lib/src/api.rs new file mode 100644 index 0000000..365c071 --- /dev/null +++ b/src/fault_lib/src/api.rs @@ -0,0 +1,49 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +use crate::{FaultSinkApi, catalog::FaultCatalog, fault_manager_sink::FaultManagerSink}; +use std::sync::{Arc, OnceLock, Weak}; + +static FAULT_SINK: OnceLock> = OnceLock::new(); +static FAULT_CATALOG: OnceLock> = OnceLock::new(); + +/// FaultApi is the long-lived handle that wires a sink and logger together. +#[derive(Clone)] +pub struct FaultApi { + _fault_sink: Arc, + _fault_catalog: Arc, +} + +impl FaultApi { + pub fn new(catalog: FaultCatalog) -> FaultApi { + // TODO: The sink should be passed as a parameter. + let sink: Arc = Arc::new(FaultManagerSink::new()); + let catalog = Arc::new(catalog); + + FAULT_SINK.set(Arc::downgrade(&sink)).expect("Fault Sink already initialized"); + FAULT_CATALOG.set(Arc::downgrade(&catalog)).expect("Fault catalog already initialized"); + sink.check_fault_catalog().expect("Failed to verify the catalog hash"); + + FaultApi { + _fault_sink: sink, + _fault_catalog: catalog, + } + } + + pub(crate) fn get_fault_sink() -> Arc { + FAULT_SINK.get().and_then(|r| r.upgrade()).expect("FaultApi not initialized") + } + + pub fn get_fault_catalog() -> Arc { + FAULT_CATALOG.get().and_then(|r| r.upgrade()).expect("FaultApi not initialized") + } +} diff --git a/src/fault_lib/src/catalog.rs b/src/fault_lib/src/catalog.rs new file mode 100644 index 0000000..301f032 --- /dev/null +++ b/src/fault_lib/src/catalog.rs @@ -0,0 +1,449 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +use common::fault::*; +use common::types::LongString; +use log::error; +use sha2::{Digest, Sha256}; +use std::borrow::Cow; +use std::panic; +use std::{collections::HashMap, fs, path::PathBuf}; + +type FaultDescriptorsMap = HashMap; +type FaultCatalogHash = Vec; +pub struct FaultCatalog { + pub id: Cow<'static, str>, + pub version: u64, + descriptors: FaultDescriptorsMap, + config_hash: FaultCatalogHash, +} + +impl FaultCatalog { + pub(crate) fn new(id: Cow<'static, str>, version: u64, descriptors: FaultDescriptorsMap, config_hash: FaultCatalogHash) -> Option { + Some(Self { + id, + version, + descriptors, + config_hash, + }) + } + + #[allow(unused)] + pub fn config_hash(&self) -> &[u8] { + &self.config_hash + } + + pub fn id(&self) -> LongString { + LongString::from_str_truncated(&self.id).expect("Fault catalog id too long") + } + + pub fn descriptor(&self, id: &FaultId) -> Option<&FaultDescriptor> { + self.descriptors.get(id) + } + + pub fn descriptors(&self) -> Vec<&FaultDescriptor> { + self.descriptors.values().collect() + } + + /// Number of descriptors in this catalog, useful for build-time validation. + pub fn len(&self) -> usize { + self.descriptors.len() + } + + pub fn is_empty(&self) -> bool { + self.descriptors.is_empty() + } +} + +/// Fault Catalog configuration structure +/// +/// Can be used for code generation of fault catalog configuration. +/// +/// # Fields +/// +/// - `id` (`Cow<'static`) - fault catalog ID . +/// - `version` (`u64`) - the version of the fault catalog. +/// - `faults` (`Vec`) - vector of fault descriptors. +/// +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct FaultCatalogConfig { + pub id: Cow<'static, str>, + pub version: u64, + pub faults: Vec, +} +pub enum FaultCatalogBuilderInput<'a> { + None, + JsonString(&'a str), + JsonFile(PathBuf), + ConfigStruct(FaultCatalogConfig), +} + +/// Fault Catalog builder +pub struct FaultCatalogBuilder<'a> { + input: FaultCatalogBuilderInput<'a>, +} + +/// Implementation of the Default trait for the fault catalog builder +/// +/// # Returns +/// +/// - `Self` - FaultCatalogBuilder structure. +/// +impl<'a> Default for FaultCatalogBuilder<'a> { + fn default() -> Self { + Self { + input: FaultCatalogBuilderInput::None, + } + } +} + +impl<'a> FaultCatalogBuilder<'a> { + /// Fault catalog builder constructor + /// + /// # Return Values + /// * FaultCatalogBuilder instance + pub fn new() -> Self { + Self::default() + } + + /// Checks if the builder has been not configured yet + /// + /// # Arguments + /// + /// - `&self` - FaultCatalogBuilder + fn check_if_not_set(&self) { + if !matches!(self.input, FaultCatalogBuilderInput::None) { + panic!("Fault Catalog builder already configured"); + } + } + + /// Configure 'FaultCatalog' with given json configuration string. + /// + /// You cannot use this function in case the configuration file has been passed before + /// # Arguments + /// + /// - `mut self` - the builder itself. + /// - `json_string` (`&'a str`) - the fault catalog configuration string in json format + /// + /// # Returns + /// + /// - `Self` - the `FaultCatalogBuilder` instance. + pub fn json_string(mut self, json_string: &'a str) -> Self { + self.check_if_not_set(); + self.input = FaultCatalogBuilderInput::JsonString(json_string); + self + } + + /// Configure the `FaultCatalog` with the given json configuration file. + /// + /// You cannot use this function in case the configuration string or fault descriptors have been + /// passed before + /// + /// # Arguments + /// + /// - `json_file` (`PathBuf`) - tha path to the `FaultCatalog` configuration file. + /// + /// # Returns + /// + /// - `Self` - The `FaultCatalogBuilder` instance . + pub fn json_file(mut self, json_file: PathBuf) -> Self { + self.check_if_not_set(); + self.input = FaultCatalogBuilderInput::JsonFile(json_file); + self + } + + pub fn cfg_struct(mut self, cfg: FaultCatalogConfig) -> Self { + self.check_if_not_set(); + self.input = FaultCatalogBuilderInput::ConfigStruct(cfg); + self + } + + /// Builds the `FaultCatalog` + /// + /// The build operation will panic in case the configuration file cannot be open + /// or the configuration json format is invalid + /// + /// # Returns + /// + /// - `FaultCatalog` - the fault catalog instance . + /// + pub fn build(self) -> FaultCatalog { + match self.input { + FaultCatalogBuilderInput::JsonString(json_str) => Self::from_json_string(json_str), + FaultCatalogBuilderInput::JsonFile(json_file) => Self::from_file(json_file), + FaultCatalogBuilderInput::ConfigStruct(cfg_struct) => Self::from_cfg_struct(cfg_struct), + + FaultCatalogBuilderInput::None => panic!("Missing fault catalog configuration"), + } + } + + /// Help function which creates `FaultCatalog` object from configuration structure + /// and calculates the `FaultCatalog` hash sum + /// + /// # Arguments + /// + /// - `cfg_struct` (`FaultCatalogConfig`) - Describe this parameter. + /// + /// # Returns + /// + /// - `FaultCatalog` - Describe the return value. + fn from_cfg_struct(cfg_struct: FaultCatalogConfig) -> FaultCatalog { + let hash_sum = Self::calc_config_hash(&cfg_struct); + FaultCatalog::new( + cfg_struct.id, + cfg_struct.version, + cfg_struct + .faults + .into_iter() + .map(|descriptor| (descriptor.id.clone(), descriptor)) + .collect(), + hash_sum, + ) + .expect("Cannot create FaultCatalog from config") + } + + /// Help function which generates fault catalog object from the the configuration json string + /// + /// + /// # Arguments + /// + /// - `json` (`&str`) - fault catalog configuration string. + /// + /// # Returns + /// + /// - `FaultCatalog` - fault catalog structure. + fn from_json_string(json: &str) -> FaultCatalog { + println!("Json: {:?}", json); + let cfg = Self::deserialize_config(json); + Self::from_cfg_struct(cfg) + } + + /// Creates the fault catalog from the given json configuration file. + /// + /// # Arguments + /// + /// - `json_path` (`PathBuf`) - path to the fault catalog json configuration file. + /// + /// # Returns + /// + /// - `FaultCatalog` - fault catalog structure. + /// + fn from_file(json_path: PathBuf) -> FaultCatalog { + let cfg_file_txt = fs::read_to_string(json_path).expect("Cannot read the json fault catalog file"); + Self::from_json_string(&cfg_file_txt) + } + + /// Calculates hash sum for the fault catalog json string + /// + /// # Arguments + /// + /// - `cfg` (`&str`) - fault catalog configuration string. + /// + /// # Returns + /// + /// - `Vec` - hash sum for the fault catalog. + /// + fn calc_config_hash(cfg: &FaultCatalogConfig) -> Vec { + let canon = serde_json::to_string(cfg).expect("Failed to serialize FaultCatalogConfig for hashing"); + Sha256::new().chain_update(canon.as_bytes()).finalize().to_vec() + } + + /// Deserialize json configuration string to the `FaultCatalogConfig` structure + /// + /// + /// + /// # Arguments + /// + /// - `config` (`&str`) - the fault catalog configuration json string . + /// + /// # Returns + /// + /// - `FaultCatalogConfig` - fault catalog configuration structure. + /// + fn deserialize_config(config: &str) -> FaultCatalogConfig { + match serde_json::from_str::(config) { + Ok(cfg) => cfg, + Err(e) => { + error!("Failed to deserialize config: {}", e); + panic!("Invalid fault catalog json"); + } + } + } +} + +#[cfg(test)] +#[cfg(not(miri))] +mod tests { + use super::*; + use crate::utils::*; + use common::debounce::DebounceMode; + use iceoryx2_bb_container::vector::Vector; + use std::time::Duration; + + /// Test helper function - creates the test fault catalog configuration structure + /// + /// # Attention Any change in this function shall also be reflected in the `./tests/ivi_fault_catalog.json` file + /// + /// # Returns + /// + /// - `FaultCatalogConfig` - fault catalog test configuration. + /// + fn create_config() -> FaultCatalogConfig { + FaultCatalogConfig { + id: "ivi".into(), + version: 1, + faults: create_descriptors(), + } + } + + /// Creates test fault descriptors + /// + /// # Attention when you change something in the returned descriptors, please edit also + /// `../tests/ivi_fault_catalog.json` file adequately + /// + /// # Returns + /// + /// - `Vec` - vector of test fault descriptors + fn create_descriptors() -> Vec { + let mut d1_compliance = ComplianceVec::new(); + let _ = d1_compliance.push(ComplianceTag::EmissionRelevant); + let _ = d1_compliance.push(ComplianceTag::SafetyCritical); + + let mut d2_compliance = ComplianceVec::new(); + let _ = d2_compliance.push(ComplianceTag::SecurityRelevant); + let _ = d2_compliance.push(ComplianceTag::SafetyCritical); + + vec![ + FaultDescriptor { + id: FaultId::Text(to_static_short_string("d1").unwrap()), + + name: to_static_short_string("Descriptor 1").unwrap(), + summary: None, + + category: FaultType::Software, + severity: FaultSeverity::Debug, + compliance: ComplianceVec::try_from(&[ComplianceTag::EmissionRelevant, ComplianceTag::SafetyCritical][..]).unwrap(), + + reporter_side_debounce: Some(DebounceMode::EdgeWithCooldown { + cooldown: Duration::from_millis(100_u64), + }), + reporter_side_reset: None, + manager_side_debounce: None, + manager_side_reset: None, + }, + FaultDescriptor { + id: FaultId::Text(to_static_short_string("d2").unwrap()), + + name: to_static_short_string("Descriptor 2").unwrap(), + summary: Some(to_static_long_string("Human-readable summary").unwrap()), + + category: FaultType::Configuration, + severity: FaultSeverity::Warn, + compliance: ComplianceVec::try_from(&[ComplianceTag::SecurityRelevant, ComplianceTag::SafetyCritical][..]).unwrap(), + + reporter_side_debounce: None, + reporter_side_reset: None, + manager_side_debounce: Some(DebounceMode::EdgeWithCooldown { + cooldown: Duration::from_millis(100_u64), + }), + manager_side_reset: None, + }, + ] + } + + #[test] + fn from_config() { + let cfg = create_config(); + + let catalog = FaultCatalogBuilder::new().cfg_struct(cfg.clone()).build(); + + let d1 = catalog + .descriptor(&FaultId::Text(to_static_short_string("d1").unwrap())) + .expect("get_descriptor failed"); + let d2 = catalog + .descriptor(&FaultId::Text(to_static_short_string("d2").unwrap())) + .expect("get_descriptor failed"); + + assert_eq!(*d1, cfg.faults[0]); + assert_eq!(*d2, cfg.faults[1]); + } + + #[test] + fn empty_config() { + let cfg = FaultCatalogConfig { + id: "".into(), + version: 7, + faults: Vec::new(), + }; + + let catalog = FaultCatalogBuilder::new().cfg_struct(cfg.clone()).build(); + let d1 = catalog.descriptor(&FaultId::Text(to_static_short_string("d1").unwrap())); + assert_eq!(d1, Option::None); + } + + #[test] + fn from_json_string() { + let cfg = create_config(); + let json_string = serde_json::to_string_pretty(&cfg).unwrap(); + + let fault_catalog = FaultCatalogBuilder::new().json_string(json_string.as_str()).build(); + let d1 = fault_catalog + .descriptor(&FaultId::Text(to_static_short_string("d1").unwrap())) + .expect("get_descriptor failed"); + let d2 = fault_catalog + .descriptor(&FaultId::Text(to_static_short_string("d2").unwrap())) + .expect("get_descriptor failed"); + + assert_eq!(*d1, cfg.faults[0]); + assert_eq!(*d2, cfg.faults[1]); + } + + #[test] + fn from_json_file() { + // Note: the path here is relative to the fault_lib directory + let fault_catalog = FaultCatalogBuilder::new() + .json_file(PathBuf::from("tests/data/ivi_fault_catalog.json")) + .build(); + let d1 = fault_catalog + .descriptor(&FaultId::Text(to_static_short_string("d1").unwrap())) + .expect("get_descriptor failed"); + let d2 = fault_catalog + .descriptor(&FaultId::Text(to_static_short_string("d2").unwrap())) + .expect("get_descriptor failed"); + // create a reference catalog config - shall be equal to the one in json + let cfg = create_config(); + + assert_eq!(*d1, cfg.faults[0]); + assert_eq!(*d2, cfg.faults[1]); + } + + #[test] + #[should_panic] + fn from_not_existing_json_file() { + let _ = FaultCatalogBuilder::new().json_file(PathBuf::from("tests/data/xxx.json")).build(); + } + + #[test] + fn hash_sum() { + let catalog_from_file = FaultCatalogBuilder::new() + .json_file(PathBuf::from("tests/data/ivi_fault_catalog.json")) + .build(); + let cfg = create_config(); + let catalog_from_cfg = FaultCatalogBuilder::new().cfg_struct(cfg.clone()).build(); + let catalog_from_json = FaultCatalogBuilder::new() + .json_string(&serde_json::to_string_pretty(&cfg).unwrap()) + .build(); + + assert_eq!(catalog_from_cfg.config_hash(), catalog_from_file.config_hash()); + assert_eq!(catalog_from_cfg.config_hash(), catalog_from_json.config_hash()); + } +} diff --git a/src/fault_lib/src/fault_manager_sink.rs b/src/fault_lib/src/fault_manager_sink.rs new file mode 100644 index 0000000..17df36b --- /dev/null +++ b/src/fault_lib/src/fault_manager_sink.rs @@ -0,0 +1,242 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +use crate::FaultApi; +use crate::ipc_worker::IpcWorker; +use crate::sink::*; +use crate::utils::to_static_long_string; +use common::fault::FaultRecord; +use common::ipc_service_name::DIAGNOSTIC_FAULT_MANAGER_HASH_CHECK_RESPONSE_SERVICE_NAME; +use common::ipc_service_type::ServiceType; +use common::sink_error::SinkError; +use common::types::{DiagnosticEvent, Sha256Vec}; +use iceoryx2::port::subscriber::Subscriber; +use iceoryx2::prelude::{NodeBuilder, ServiceName}; +use log::*; +use std::time::{Duration, Instant}; +use std::{ + sync::mpsc, + thread::{self, JoinHandle}, +}; + +#[allow(unused_imports)] +use mockall::automock; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FaultManagerError { + SendError(String), + Timeout, +} + +impl From for SinkError { + fn from(err: FaultManagerError) -> Self { + match err { + FaultManagerError::SendError(msg) => SinkError::Other(Box::leak(msg.into_boxed_str())), + FaultManagerError::Timeout => SinkError::Other("timeout"), + } + } +} + +/// Request channel type used by the sink_thread to receive events +pub type WorkerReceiver = mpsc::Receiver; + +#[derive(Debug)] +pub(crate) enum WorkerMsg { + /// transports start message with the parent thread which will be unparked when the sink_thread thread is up and running + Start(std::thread::Thread), + + /// Sent by the FaultManagerSink when the fault monitor reports an event. + Event { event: Box }, + + /// Terminate the FaultManagerSink working thread + Exit, +} + +const TIMEOUT: Duration = Duration::from_millis(500); + +pub struct FaultManagerSink { + sink_sender: mpsc::Sender, + sink_thread: Option>, + hash_check_response_subscriber: Option>, +} + +impl FaultManagerSink { + pub(crate) fn new() -> Self { + let (tx, rx) = mpsc::channel(); + let handle = thread::Builder::new() + .name("fault_client_worker".into()) + .spawn(move || { + let ipc_worker = IpcWorker::new(rx); + ipc_worker.run(); + }) + .expect("Cannot spawn Fault Mgr Client sink thread"); + + tx.send(WorkerMsg::Start(thread::current())).expect("Couldn't start the worker thread"); + thread::park(); + + let node = NodeBuilder::new() + .create::() + .expect("Failed to create the fault service node"); + let hash_check_response_service_name = + ServiceName::new(DIAGNOSTIC_FAULT_MANAGER_HASH_CHECK_RESPONSE_SERVICE_NAME).expect("Failed to create the fault service name"); + let hash_check_response_service = node + .service_builder(&hash_check_response_service_name) + .publish_subscribe::() + .open_or_create() + .expect("Failed to create the hash transmitter service"); + let hash_check_response_subscriber = hash_check_response_service + .subscriber_builder() + .create() + .expect("Failed to create the hash transmitter client"); + Self { + sink_sender: tx, + sink_thread: Some(handle), + hash_check_response_subscriber: Some(hash_check_response_subscriber), + } + } + + fn listen_hash_check_response(&self) -> Result { + let start = Instant::now(); + loop { + if let Some(msg) = self + .hash_check_response_subscriber + .as_ref() + .unwrap() + .receive() + .map_err(|_| SinkError::TransportDown)? + { + return Ok(*msg.payload()); + } + if start.elapsed() >= TIMEOUT { + return Err(SinkError::Timeout); + } + std::thread::sleep(Duration::from_millis(10)); + } + } +} + +/// API to be used by the modules of the fault-lib which need to communicate with +/// Diagnostic Fault Manager. This trait shall never become public +impl FaultSinkApi for FaultManagerSink { + fn publish(&self, path: &str, record: FaultRecord) -> Result<(), SinkError> { + let event = DiagnosticEvent::Fault((to_static_long_string(path).expect("Failed to serialize path"), record)); + self.sink_sender + .send(WorkerMsg::Event { event: Box::new(event) }) + .map_err(|e| FaultManagerError::SendError(format!("Cannot send event: {e}")))?; + Ok(()) + } + + fn check_fault_catalog(&self) -> Result { + let catalog = FaultApi::get_fault_catalog(); + let event = DiagnosticEvent::Hash((catalog.id(), Sha256Vec::try_from(catalog.config_hash()).unwrap())); + // this send immediately returns + let result = self + .sink_sender + .send(WorkerMsg::Event { event: Box::new(event) }) + .map_err(|e| SinkError::from(FaultManagerError::SendError(format!("Cannot send event: {e}")))); + if result.is_err() { + return Err(result.err().unwrap()); + } + // this will wait for the response + self.listen_hash_check_response() + } +} + +impl Drop for FaultManagerSink { + fn drop(&mut self) { + debug!("Drop FaultManagerSink"); + if let Some(hndl) = self.sink_thread.take() { + let _ = self.sink_sender.send(WorkerMsg::Exit); + + let current_id = std::thread::current().id(); + let worker_id = hndl.thread().id(); + + if current_id == worker_id { + error!("Skipping join: drop called from the sink_thread thread"); + return; + } + + debug!("Joining sink_thread thread"); + if let Err(err) = hndl.join() { + error!("Worker thread panicked: {:?}", err); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::*; + use common::FaultId; + use common::types::*; + use std::time::Duration; + + fn new_for_publish_test() -> (FaultManagerSink, mpsc::Receiver) { + let (tx, rx) = mpsc::channel(); + let client = FaultManagerSink { + sink_sender: tx, + sink_thread: None, + hash_check_response_subscriber: None, + }; + (client, rx) + } + + fn new_for_drop_test() -> (FaultManagerSink, mpsc::Receiver) { + let (tx, rx) = mpsc::channel(); + let handle = thread::spawn(|| { + thread::sleep(Duration::from_millis(1)); + }); + let client = FaultManagerSink { + sink_sender: tx, + sink_thread: Some(handle), + hash_check_response_subscriber: None, + }; + (client, rx) + } + + #[test] + fn test_publish_sends_event_message() { + let (client, rx) = new_for_publish_test(); + let fault_id = FaultId::Numeric(42); + let fault_name = ShortString::from_bytes("Test Fault".as_bytes()).unwrap(); + let desc = stub_descriptor(fault_id, fault_name, None, None); + let path = "test/path"; + + let result = ::publish(&client, path, stub_record(desc.clone())); + assert!(result.is_ok()); + + match rx.recv_timeout(Duration::from_millis(50)).unwrap() { + WorkerMsg::Event { event } => match &*event { + DiagnosticEvent::Fault((path, record)) => { + assert_eq!(path.to_string(), "test/path"); + assert_eq!(record.id, FaultId::Numeric(42)); + } + DiagnosticEvent::Hash(_) => { + panic!("Expected Fault event, got Hash"); + } + }, + other => panic!("Received wrong message type: {:?}", other), + } + } + + #[test] + fn test_drop_sends_exit_message() { + let (client, rx) = new_for_drop_test(); + drop(client); + + match rx.recv_timeout(Duration::from_millis(50)).unwrap() { + WorkerMsg::Exit => {} + other => panic!("Received wrong message type, expected Exit: {:?}", other), + } + } +} diff --git a/src/fault_lib/src/ipc_worker.rs b/src/fault_lib/src/ipc_worker.rs new file mode 100644 index 0000000..10b2d0b --- /dev/null +++ b/src/fault_lib/src/ipc_worker.rs @@ -0,0 +1,125 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// +use crate::fault_manager_sink::{WorkerMsg, WorkerReceiver}; +use common::ipc_service_name::DIAGNOSTIC_FAULT_MANAGER_EVENT_SERVICE_NAME; +use common::ipc_service_type::ServiceType; +use common::sink_error::SinkError; +use common::types::DiagnosticEvent; +use iceoryx2::port::publisher::Publisher; +use iceoryx2::prelude::{NodeBuilder, ServiceName}; +use log::*; + +#[allow(unused_imports)] +use mockall::automock; + +pub struct IpcWorker { + #[allow(dead_code)] + sink_receiver: WorkerReceiver, + diagnostic_publisher: Option>, +} + +impl IpcWorker { + pub fn new(sink_receiver: WorkerReceiver) -> Self { + let node = NodeBuilder::new() + .create::() + .expect("Failed to create the fault service node"); + let event_publisher_service_name = + ServiceName::new(DIAGNOSTIC_FAULT_MANAGER_EVENT_SERVICE_NAME).expect("Failed to create the fault service name"); + let event_publisher_service = node + .service_builder(&event_publisher_service_name) + .publish_subscribe::() + .open_or_create() + .expect("Failed to create the hash transmitter service"); + let publisher = event_publisher_service + .publisher_builder() + .create() + .expect("Failed to create the hash transmitter client"); + + Self { + sink_receiver, + diagnostic_publisher: Some(publisher), + } + } + + fn publish_event(&self, event: DiagnosticEvent) -> Result<(), SinkError> { + let sample = self + .diagnostic_publisher + .as_ref() + .unwrap() + .loan_uninit() + .map_err(|_| SinkError::TransportDown)?; + let sample = sample.write_payload(event); + match sample.send().map_err(|_| SinkError::TransportDown) { + Ok(_) => { + debug!("Event successfully sent!"); + Ok(()) + } + Err(e) => Err(e), + } + } + + pub fn run(&self) { + while let Ok(msg) = self.sink_receiver.recv() { + match msg { + WorkerMsg::Start(parent) => { + debug!("Diag IPC worker running"); + parent.unpark(); + } + WorkerMsg::Event { event } => { + self.publish_event(*event).unwrap_or_else(|_| error!("publish_event failed")); + } + WorkerMsg::Exit => { + info!("FaultMgrClient worker ends"); + break; + } + } + } + } +} + +#[cfg(test)] +#[cfg(not(miri))] +mod tests { + use super::*; + use crate::fault_manager_sink::WorkerMsg; + use std::sync::mpsc; + use std::thread; + use std::time::Duration; + + #[test] + fn test_fault_sink_start_and_exit_with_timeout() { + let (tx, rx) = mpsc::channel::(); + let fault_sink = IpcWorker::new(rx); + let handle = thread::spawn(move || fault_sink.run()); + + tx.send(WorkerMsg::Start(thread::current())).unwrap(); + tx.send(WorkerMsg::Exit).unwrap(); + + let (join_tx, join_rx) = mpsc::channel(); + + thread::spawn(move || { + let join_result = handle.join(); + join_tx.send(join_result).ok(); + }); + + let test_timeout = Duration::from_secs(5); + match join_rx.recv_timeout(test_timeout) { + Ok(Ok(())) => {} + Ok(Err(panic_err)) => { + std::panic::resume_unwind(panic_err); + } + Err(_) => { + panic!("Test failed: Worker thread did not exit within 5 seconds"); + } + } + } +} diff --git a/src/fault_lib/src/lib.rs b/src/fault_lib/src/lib.rs new file mode 100644 index 0000000..5215fe8 --- /dev/null +++ b/src/fault_lib/src/lib.rs @@ -0,0 +1,32 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// +#![forbid(unsafe_code)] // enforce safe Rust across the crate +// The public surface collects the building blocks for reporters, descriptors, +// and sinks so callers can just `use fault_lib::*` and go. +pub mod api; +pub mod catalog; +pub mod reporter; +pub mod sink; + +mod fault_manager_sink; +mod ipc_worker; + +pub use api::FaultApi; +// Re-export the main user-facing pieces, this keeps the crate ergonomic without +// forcing consumers to dig through modules. +// pub use api::{FaultApi, Reporter}; +// pub use catalog::FaultCatalog; +pub use sink::{FaultSinkApi, LogHook}; + +pub mod utils; + +pub mod test_utils; diff --git a/src/fault_lib/src/reporter.rs b/src/fault_lib/src/reporter.rs new file mode 100644 index 0000000..1a05f5f --- /dev/null +++ b/src/fault_lib/src/reporter.rs @@ -0,0 +1,139 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// +use crate::{FaultApi, sink::*}; +use common::{fault::*, sink_error::*, types::*}; +use std::sync::Arc; + +// Per-component defaults that get baked into a Reporter instance. +#[derive(Debug, Clone)] +pub struct ReporterConfig { + pub source: common::ids::SourceId, + pub lifecycle_phase: LifecyclePhase, + /// Optional per-reporter defaults (e.g., common metadata). + pub default_env_data: MetadataVec, +} + +pub trait ReporterApi { + fn new(id: &FaultId, config: ReporterConfig) -> Option + where + Self: Sized; + fn create_record(&self, lifecycle_stage: LifecycleStage) -> FaultRecord; + fn publish(&mut self, path: &str, record: FaultRecord) -> Result<(), SinkError>; +} + +pub struct Reporter { + sink: Arc, + descriptor: FaultDescriptor, + config: ReporterConfig, +} + +impl ReporterApi for Reporter { + fn new(id: &FaultId, config: ReporterConfig) -> Option { + Some(Self { + sink: FaultApi::get_fault_sink(), + descriptor: FaultApi::get_fault_catalog().descriptor(id)?.clone(), + config, + }) + } + + // TODO: Discuss: + // - Should severity be modifiable, and on the record? + // - Why have an API to modifying env_data instead of just allowing the user to modify the vector. + // - When should time be set? + fn create_record(&self, lifecycle_stage: LifecycleStage) -> FaultRecord { + FaultRecord { + id: self.descriptor.id.clone(), + time: IpcTimestamp { + seconds_since_epoch: (0), + nanoseconds: (0), + }, // TODO: When should "now" be? Create time? Publish time? + source: self.config.source.clone(), + lifecycle_phase: self.config.lifecycle_phase, + lifecycle_stage, + env_data: self.config.default_env_data.clone(), + } + } + + fn publish(&mut self, path: &str, record: FaultRecord) -> Result<(), SinkError> { + // Notes on debouncing: + // - Use the record's lifecycle_stage to determine whether the fault is active or not. + // - Use the descriptor's reporter_side_debounce to determine debounce policy for the reporter. + + // To be discussed: + // - What happens to record's source and metadata when the debounce discards the record? + + self.sink.publish(path, record) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sink::MockFaultSinkApi; + use crate::test_utils::*; + use crate::utils::to_static_short_string; + + #[test] + fn create_record() { + let reporter = Reporter { + sink: Arc::new(MockFaultSinkApi::new()), + descriptor: stub_descriptor(FaultId::Numeric(42), to_static_short_string("Test fault").unwrap(), None, None), + config: stub_config(), + }; + + let record = reporter.create_record(LifecycleStage::Passed); + + assert_eq!(record.id, FaultId::Numeric(42)); + assert_eq!(record.source, stub_source()); + assert_eq!(record.lifecycle_phase, LifecyclePhase::Running); + assert_eq!(record.lifecycle_stage, LifecycleStage::Passed); + } + + #[test] + fn publsh_success() { + let mut mock_sink = MockFaultSinkApi::new(); + mock_sink.expect_publish().once().returning(|path, record| { + assert_eq!(path, "test/path"); + assert_eq!(record.id, FaultId::Numeric(42)); + assert_eq!(record.source, stub_source()); + assert_eq!(record.lifecycle_phase, LifecyclePhase::Running); + assert_eq!(record.lifecycle_stage, LifecycleStage::Passed); + + Ok(()) + }); + + let mut reporter = Reporter { + sink: Arc::new(mock_sink), + descriptor: stub_descriptor(FaultId::Numeric(42), to_static_short_string("Test fault").unwrap(), None, None), + config: stub_config(), + }; + + let record = reporter.create_record(LifecycleStage::Passed); + + assert!(reporter.publish("test/path", record).is_ok()); + } + + #[test] + fn publish_fail() { + let mut mock_sink = MockFaultSinkApi::new(); + mock_sink.expect_publish().once().returning(|_, _| Err(SinkError::TransportDown)); + + let mut reporter = Reporter { + sink: Arc::new(mock_sink), + descriptor: stub_descriptor(FaultId::Numeric(42), to_static_short_string("Test fault").unwrap(), None, None), + config: stub_config(), + }; + + let record = reporter.create_record(LifecycleStage::Passed); + assert_eq!(reporter.publish("test/path", record), Err(SinkError::TransportDown)); + } +} diff --git a/src/fault_lib/src/sink.rs b/src/fault_lib/src/sink.rs new file mode 100644 index 0000000..084af45 --- /dev/null +++ b/src/fault_lib/src/sink.rs @@ -0,0 +1,41 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +use common::fault::FaultRecord; +use common::sink_error::SinkError; + +#[allow(unused_imports)] +use mockall::automock; + +// Boundary traits for anything that has side-effects (logging + IPC). + +/// Hook to ensure that reporting a fault additionally results in a log entry. +/// Default impl can forward to log. +pub trait LogHook: Send + Sync + 'static { + fn on_report(&self, record: &FaultRecord); +} + +/// Sink abstracts the transport to the Diagnostic Fault Manager. +/// +/// Non-blocking contract: +/// - MUST return quickly (enqueue only) without waiting on IPC/network/disk. +/// - SHOULD avoid allocating excessively or performing locking that can contend with hot paths. +/// - Backpressure and retry are internal; caller only gets enqueue success/failure. +/// - Lifetime: installed once in `FaultApi::new` and lives for the duration of the process. +/// +/// Implementations can be S-CORE IPC. +#[cfg_attr(test, automock)] +pub trait FaultSinkApi: Send + Sync + 'static { + /// Enqueue a record for delivery to the Diagnostic Fault Manager. + fn publish(&self, path: &str, record: FaultRecord) -> Result<(), SinkError>; + fn check_fault_catalog(&self) -> Result; +} diff --git a/src/fault_lib/src/test_utils.rs b/src/fault_lib/src/test_utils.rs new file mode 100644 index 0000000..c754c45 --- /dev/null +++ b/src/fault_lib/src/test_utils.rs @@ -0,0 +1,111 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// +use crate::reporter::ReporterConfig; +use crate::utils::*; +use common::config::ResetPolicy; +use common::debounce::DebounceMode; +use common::fault::*; +use common::ids::*; +use common::types::*; +use std::string::String; +use std::time::Duration; + +#[allow(dead_code)] +pub fn stub_source() -> SourceId { + SourceId { + entity: to_static_short_string("source").unwrap(), + ecu: Some(ShortString::from_bytes("ECU-A".as_bytes()).unwrap()), + domain: Some(to_static_short_string("ADAS").unwrap()), + sw_component: Some(to_static_short_string("Perception").unwrap()), + instance: Some(to_static_short_string("0").unwrap()), + } +} + +#[allow(dead_code)] +pub fn stub_config() -> ReporterConfig { + ReporterConfig { + source: stub_source(), + lifecycle_phase: LifecyclePhase::Running, + default_env_data: MetadataVec::new(), + } +} + +#[allow(dead_code)] +pub fn stub_descriptor(id: FaultId, name: ShortString, debounce: Option, reset: Option) -> FaultDescriptor { + FaultDescriptor { + id, + name, + summary: None, + category: FaultType::Software, + severity: FaultSeverity::Warn, + compliance: ComplianceVec::new(), + reporter_side_debounce: debounce, + reporter_side_reset: reset, + manager_side_debounce: None, + manager_side_reset: None, + } +} + +#[allow(dead_code)] +pub fn stub_record(desc: FaultDescriptor) -> FaultRecord { + FaultRecord { + id: desc.id, + time: IpcTimestamp::default(), + source: stub_source(), + lifecycle_phase: LifecyclePhase::Running, + lifecycle_stage: LifecycleStage::NotTested, + env_data: MetadataVec::new(), + } +} + +pub fn create_dummy_descriptors() -> Vec { + let d1 = FaultDescriptor { + id: FaultId::Text(to_static_short_string("d1").unwrap()), + + name: to_static_short_string("Descriptor 1").unwrap(), + summary: None, + + category: FaultType::Software, + severity: FaultSeverity::Debug, + compliance: ComplianceVec::try_from(&[ComplianceTag::EmissionRelevant, ComplianceTag::SafetyCritical][..]).unwrap(), + + reporter_side_debounce: Some(DebounceMode::EdgeWithCooldown { + cooldown: Duration::from_millis(100_u64), + }), + reporter_side_reset: None, + manager_side_debounce: None, + manager_side_reset: None, + }; + + let d2 = FaultDescriptor { + id: FaultId::Text(to_static_short_string("d2").unwrap()), + + name: to_static_short_string("Descriptor 2").unwrap(), + summary: Some(to_static_long_string("Human-readable summary").unwrap()), + + category: FaultType::Configuration, + severity: FaultSeverity::Warn, + compliance: ComplianceVec::try_from(&[ComplianceTag::SecurityRelevant, ComplianceTag::SafetyCritical][..]).unwrap(), + + reporter_side_debounce: None, + reporter_side_reset: None, + manager_side_debounce: Some(DebounceMode::EdgeWithCooldown { + cooldown: Duration::from_millis(100_u64), + }), + manager_side_reset: None, + }; + vec![d1, d2] +} + +pub fn load_dummy_config_file() -> String { + serde_json::to_string(&create_dummy_descriptors()).expect("serde_json::to_string failed") +} diff --git a/src/fault_lib/src/utils.rs b/src/fault_lib/src/utils.rs new file mode 100644 index 0000000..031f945 --- /dev/null +++ b/src/fault_lib/src/utils.rs @@ -0,0 +1,91 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// +use common::types::*; +use iceoryx2_bb_container::string::*; + +macro_rules! __fault_descriptor_optional_str { + () => { + None + }; + ($value:literal) => { + Some(::std::borrow::Cow::Borrowed($value)) + }; +} + +// pub id: FaultId, + +// pub name: ShortString, +// pub summary: Option, + +// pub category: FaultType, +// pub severity: FaultSeverity, +// pub compliance: ComplianceVec, + +// pub reporter_side_debounce: Option, +// pub reporter_side_reset: Option, +// pub manager_side_debounce: Option, +// pub manager_side_reset: Option, +// } + +#[doc(hidden)] +#[macro_export] +macro_rules! __fault_descriptor_compliance_vec { + // No compliance tags => empty ComplianceVec + () => {{ + let v: common::fault::ComplianceVec = common::fault::ComplianceVec::new(); + v + }}; + // One or more tags => fill the ComplianceVec + ($($ctag:expr),+ $(,)?) => {{ + let mut v: common::fault::ComplianceVec = common::fault::ComplianceVec::new(); + $( + v.push($ctag); + )+ + v + }}; +} + +#[macro_export] +macro_rules! fault_descriptor { + // Minimal form; policies can be added via builder functions if desired. + ( + id = $id:expr, + name = $name:literal, + kind = $kind:expr, + severity = $sev:expr + $(, compliance = [$($ctag:expr),* $(,)?])? + $(, summary = $summary:literal)? + $(, debounce = $debounce:expr)? + $(, reset = $reset:expr)? + ) => {{ + common::fault::FaultDescriptor { + id: $id, + name: $name, + category: $kind, + severity: $sev, + compliance: $crate::__fault_descriptor_compliance_vec!($($($ctag),*)?), + reporter_side_debounce: $(Some($debounce))?, + reporter_side_reset: $(Some($reset))?, + manager_side_debounce : None , + manager_side_reset : None, + summary: $crate::utils::__fault_descriptor_optional_str!($($summary)?), + } + }}; +} + +pub fn to_static_short_string>(input: T) -> Result { + StaticString::try_from(input.as_ref()) +} + +pub fn to_static_long_string>(input: T) -> Result { + StaticString::try_from(input.as_ref()) +} diff --git a/src/fault_lib/tests/data/hvac_fault_catalog.json b/src/fault_lib/tests/data/hvac_fault_catalog.json new file mode 100644 index 0000000..3291a87 --- /dev/null +++ b/src/fault_lib/tests/data/hvac_fault_catalog.json @@ -0,0 +1,53 @@ +{ + "id": "hvac", + "version": 3, + "faults": [ + { + "id": { + "Numeric": 28673 + }, + "name": "CabinTempSensorStuck", + "summary": null, + "category": "Communication", + "severity": "Error", + "compliance": [ + "EmissionRelevant" + ], + "reporter_side_debounce": { + "HoldTime": { + "duration": { + "secs": 60, + "nanos": 0 + } + } + }, + "reporter_side_reset": null, + "manager_side_debounce": null, + "manager_side_reset": null + }, + { + "id": { + "Text": "hvac.blower.speed_sensor_mismatch" + }, + "name": "BlowerSpeedMismatch", + "summary": "Human-readable summary", + "category": "Communication", + "severity": "Error", + "compliance": [ + "SecurityRelevant", + "SafetyCritical" + ], + "reporter_side_debounce": null, + "reporter_side_reset": null, + "manager_side_debounce": { + "EdgeWithCooldown": { + "cooldown": { + "secs": 0, + "nanos": 100000000 + } + } + }, + "manager_side_reset": null + } + ] +} \ No newline at end of file diff --git a/src/fault_lib/tests/data/ivi_fault_catalog.json b/src/fault_lib/tests/data/ivi_fault_catalog.json new file mode 100644 index 0000000..a756a9c --- /dev/null +++ b/src/fault_lib/tests/data/ivi_fault_catalog.json @@ -0,0 +1,54 @@ +{ + "id": "ivi", + "version": 1, + "faults": [ + { + "id": { + "Text": "d1" + }, + "name": "Descriptor 1", + "summary": null, + "category": "Software", + "severity": "Debug", + "compliance": [ + "EmissionRelevant", + "SafetyCritical" + ], + "reporter_side_debounce": { + "EdgeWithCooldown": { + "cooldown": { + "secs": 0, + "nanos": 100000000 + } + } + }, + "reporter_side_reset": null, + "manager_side_debounce": null, + "manager_side_reset": null + }, + { + "id": { + "Text": "d2" + }, + "name": "Descriptor 2", + "summary": "Human-readable summary", + "category": "Configuration", + "severity": "Warn", + "compliance": [ + "SecurityRelevant", + "SafetyCritical" + ], + "reporter_side_debounce": null, + "reporter_side_reset": null, + "manager_side_debounce": { + "EdgeWithCooldown": { + "cooldown": { + "secs": 0, + "nanos": 100000000 + } + } + }, + "manager_side_reset": null + } + ] +} \ No newline at end of file diff --git a/src/ids.rs b/src/ids.rs deleted file mode 100644 index 744e22f..0000000 --- a/src/ids.rs +++ /dev/null @@ -1,58 +0,0 @@ -/* -* Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) -* -* 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 -*/ - -use std::{borrow::Cow, fmt}; - -// Lightweight identifiers that keep fault attribution consistent across the fleet. - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum FaultId { - Numeric(u32), // e.g., DTC-like - Text(Cow<'static, str>), // human-stable symbolic ID (runtime or static) - Uuid([u8; 16]), // global uniqueness if needed -} - -impl FaultId { - /// Convenience for constructing a textual ID from either a static string or owned `String`. - pub fn text(value: impl Into>) -> Self { - Self::Text(value.into()) - } - - /// `const` helper so descriptors can be defined in static contexts. - pub const fn text_const(value: &'static str) -> Self { - Self::Text(Cow::Borrowed(value)) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct SourceId { - pub entity: &'static str, // e.g., "ADAS.Perception", "HVAC" - pub ecu: Option<&'static str>, // e.g., "ECU-A" - pub domain: Option<&'static str>, // e.g., "ADAS", "IVI" - pub sw_component: Option<&'static str>, - pub instance: Option<&'static str>, // allow N instances -} - -impl fmt::Display for SourceId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let ecu = self.ecu.unwrap_or("-"); - let dom = self.domain.unwrap_or("-"); - let comp = self.sw_component.unwrap_or("-"); - let inst = self.instance.unwrap_or("-"); - write!( - f, - "{}@ecu:{} dom:{} comp:{} inst:{}", - self.entity, ecu, dom, comp, inst - ) - } -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 97588c9..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,37 +0,0 @@ -/* -* Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) -* -* 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 -*/ - -#![forbid(unsafe_code)] // enforce safe Rust across the crate -#![feature(const_option_ops)] // -#![feature(const_trait_impl)] -// The public surface collects the building blocks for reporters, descriptors, -// and sinks so callers can just `use fault_lib::*` and go. -pub mod api; -pub mod catalog; -pub mod config; -pub mod ids; -pub mod model; -pub mod sink; -pub mod utils; - -// Re-export the main user-facing pieces, this keeps the crate ergonomic without -// forcing consumers to dig through modules. -pub use api::{FaultApi, Reporter}; -pub use catalog::FaultCatalog; -pub use config::{DebouncePolicy, ReportOptions, ReporterConfig, ResetPolicy}; -pub use ids::{FaultId, SourceId}; -pub use model::{ - ComplianceTag, FaultDescriptor, FaultLifecycleStage, FaultRecord, FaultSeverity, FaultType, - KeyValue, LifecyclePhase, -}; -pub use sink::{FaultSink, LogHook}; diff --git a/src/model.rs b/src/model.rs deleted file mode 100644 index 71a40da..0000000 --- a/src/model.rs +++ /dev/null @@ -1,130 +0,0 @@ -/* -* Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) -* -* 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 -*/ - -use crate::FaultId; -// use crate::DebouncePolicy; -use std::{borrow::Cow, time::SystemTime}; - -// Shared domain types that move between reporters, sinks, and integrators. - -/// Align severities to DLT-like levels, stable for logging & UI filters. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum FaultSeverity { - Trace, - Debug, - Info, - Warn, - Error, - Fatal, -} - -/// Canonical fault type buckets used for analytics and tooling. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum FaultType { - Hardware, - Software, - Communication, - Configuration, - Timing, - Power, - /// Escape hatch for domain-specific groupings until the enum grows. - Custom(&'static str), -} - -/// Compliance/regulatory tags drive escalation, retention, and workflow. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ComplianceTag { - EmissionRelevant, - SafetyCritical, - SecurityRelevant, - LegalHold, -} - -/// Lifecycle phase of the reporting component/system (for policy gating). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum LifecyclePhase { - Init, - Running, - Suspend, - Resume, - Shutdown, -} - -/// Simplified internal test lifecycle aligned with ISO 14229-1 style semantics. -/// DTC lifecycle (confirmation, pending, aging, etc.) is handled centrally by the DFM. -/// The fault-lib only tracks raw test pass/fail progression + pre-states around debounce. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum FaultLifecycleStage { - NotTested, // test not executed yet for this reporting window - PreFailed, // initial failure observed but still within debounce/pending window - Failed, // confirmed failure (debounce satisfied / threshold met) - PrePassed, // transitioning back to healthy; stability window accumulating - Passed, // test executed and passed (healthy condition) -} - -/// Minimal, typed environment data; keep serde-agnostic at the API edge. -#[derive(Debug, Clone)] -pub struct KeyValue { - pub key: &'static str, - /// Values stay stringly-typed so logging/IPC layers stay decoupled. - pub value: String, -} - -/// Immutable, compile-time describer of a fault type (identity + defaults). -#[derive(Debug, Clone)] -pub struct FaultDescriptor { - pub id: crate::ids::FaultId, - pub name: Cow<'static, str>, - pub fault_type: FaultType, - pub default_severity: FaultSeverity, - pub compliance: Cow<'static, [ComplianceTag]>, - /// Default debounce/reset; can be overridden per-report via ReportOptions. - pub debounce: Option, - pub reset: Option, - /// Human-facing details. - pub summary: Option>, -} - -/// Concrete record produced on each report() call, also logged. -/// Contains only runtime-mutable data; static configuration lives in FaultDescriptor. -#[derive(Debug, Clone)] -pub struct FaultRecord { - pub fault_id: FaultId, - pub time: SystemTime, - pub severity: FaultSeverity, - pub source: crate::ids::SourceId, - pub lifecycle_phase: LifecyclePhase, - pub stage: FaultLifecycleStage, - pub environment_data: Vec, -} - -impl FaultRecord { - /// Append environment data (mutable) - pub fn add_environment_data(&mut self, key: &'static str, value: String) { - self.environment_data.push(KeyValue { key, value }); - self.time = SystemTime::now(); - } - - /// Update lifecycle stage (mutable) - pub fn update_stage(&mut self, stage: FaultLifecycleStage) { - self.stage = stage; - self.time = SystemTime::now(); - } - - /// Update severity (mutable) - pub fn update_severity(&mut self, severity: FaultSeverity) { - self.severity = severity; - self.time = SystemTime::now(); - } - -} diff --git a/src/sink.rs b/src/sink.rs deleted file mode 100644 index 38bc431..0000000 --- a/src/sink.rs +++ /dev/null @@ -1,50 +0,0 @@ -/* -* Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) -* -* 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 -*/ - -use crate::model::FaultRecord; - -// Boundary traits for anything that has side-effects (logging + IPC). - -/// Hook to ensure that reporting a fault additionally results in a log entry. -/// Default impl can forward to log. -pub trait LogHook: Send + Sync + 'static { - fn on_report(&self, record: &FaultRecord); -} - -/// Sink abstracts the transport to the Diagnostic Fault Manager. -/// -/// Non-blocking contract: -/// - MUST return quickly (enqueue only) without waiting on IPC/network/disk. -/// - SHOULD avoid allocating excessively or performing locking that can contend with hot paths. -/// - Backpressure and retry are internal; caller only gets enqueue success/failure. -/// - Lifetime: installed once in `FaultApi::new` and lives for the duration of the process. -/// -/// Implementations can be S-CORE IPC. -pub trait FaultSink: Send + Sync + 'static { - /// Enqueue a record for delivery to the Diagnostic Fault Manager. - fn publish(&self, record: &FaultRecord) -> Result<(), SinkError>; -} - -#[derive(thiserror::Error, Debug)] -pub enum SinkError { - #[error("transport unavailable")] - TransportDown, - #[error("rate limited")] - RateLimited, - #[error("permission denied")] - PermissionDenied, - #[error("invalid descriptor: {0}")] - BadDescriptor(&'static str), - #[error("other: {0}")] - Other(&'static str), -} diff --git a/src/utils.rs b/src/utils.rs deleted file mode 100644 index 3334116..0000000 --- a/src/utils.rs +++ /dev/null @@ -1,51 +0,0 @@ -/* -* Copyright (c) 2025 The Contributors to Eclipse OpenSOVD (see CONTRIBUTORS) -* -* 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 -*/ - -// Small macro helpers that keep descriptor definitions tidy in user code. - -#[doc(hidden)] -#[macro_export] -macro_rules! __fault_descriptor_optional_str { - () => { - None - }; - ($value:literal) => { - Some(::std::borrow::Cow::Borrowed($value)) - }; -} - -#[macro_export] -macro_rules! fault_descriptor { - // Minimal form; policies can be added via builder functions if desired. - ( - id = $id:expr, - name = $name:literal, - kind = $kind:expr, - severity = $sev:expr - $(, compliance = [$($ctag:expr),* $(,)?])? - $(, summary = $summary:literal)? - $(, debounce = $debounce:expr)? - $(, reset = $reset:expr)? - ) => {{ - $crate::model::FaultDescriptor { - id: $id, - name: ::std::borrow::Cow::Borrowed($name), - fault_type: $kind, - default_severity: $sev, - compliance: ::std::borrow::Cow::Borrowed(&[$($($ctag),*,)?]), - debounce: $(Some($debounce))?, - reset: $(Some($reset))?, - summary: $crate::__fault_descriptor_optional_str!($($summary)?), - } - }}; -} diff --git a/src/xtask/Cargo.toml b/src/xtask/Cargo.toml new file mode 100644 index 0000000..7c40ee4 --- /dev/null +++ b/src/xtask/Cargo.toml @@ -0,0 +1,5 @@ +[package] +name = "xtask" +version.workspace = true +edition.workspace = true +readme.workspace = true \ No newline at end of file diff --git a/src/xtask/src/main.rs b/src/xtask/src/main.rs new file mode 100644 index 0000000..2b81162 --- /dev/null +++ b/src/xtask/src/main.rs @@ -0,0 +1,249 @@ +// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// + +use std::collections::HashMap; +use std::env; +use std::fs; +use std::path::Path; +use std::process::{Command, exit}; + +fn main() { + let mut args = env::args().skip(1); // skip the binary name + + // println!("{:?}", args.next()); + let Some(command) = args.next() else { + print_usage_and_exit(); + }; + + // Split into env vars (KEY=VALUE) and passthrough args + let mut cli_env_vars = HashMap::new(); + let mut passthrough_args = Vec::new(); + + for arg in args { + if let Some((key, value)) = arg.split_once('=') { + cli_env_vars.insert(key.to_string(), value.to_string()); + } else { + passthrough_args.push(arg); + } + } + + let mut envs = HashMap::new(); + + match command.as_str() { + "build" => { + debug_build(envs, cli_env_vars, &passthrough_args); + } + "clippy" => { + clippy(envs, cli_env_vars, &passthrough_args); + } + "run" => { + run_build("debug_build", &["run"], envs, cli_env_vars, &passthrough_args); + } + "build:release" => { + run_build("release_build", &["build", "--release"], envs, cli_env_vars, &passthrough_args); + } + "run:release" => { + run_build("release_build", &["run", "--release"], envs, cli_env_vars, &passthrough_args); + } + "build:test" | "test" => { + test(envs, cli_env_vars, &passthrough_args); + } + "build:loom" => { + envs.insert("RUSTFLAGS".into(), "--cfg loom".into()); + run_build("loom_build", &["test", "--release"], envs, cli_env_vars, &passthrough_args); + } + "build:qnx_x86_64" => { + run_build( + "", + &["+qnx7.1_rust", "build", "--target", "x86_64-pc-nto-qnx710"], + envs, + cli_env_vars, + &passthrough_args, + ); + } + "build:qnx_arm" => { + run_build( + "", + &["+qnx7.1_rust", "build", "--target", "aarch64-unknown-nto-qnx710"], + envs, + cli_env_vars, + &passthrough_args, + ); + } + "check_lic" => { + check_license_header(); + } + "check" => { + check_license_header(); + run_command( + &["fmt", "--", "--check"], + HashMap::default(), + &passthrough_args, + Some("Wrong formatting@"), + ); + debug_build(envs.clone(), cli_env_vars.clone(), &passthrough_args); + clippy(envs.clone(), cli_env_vars.clone(), &passthrough_args); + test(envs, cli_env_vars, &passthrough_args); + } + "build:scenarios" => { + run_build( + "debug_build", + &["build", "--manifest-path", "component_integration_tests/rust_test_scenarios/Cargo.toml"], + envs, + cli_env_vars, + &passthrough_args, + ); + } + "run:scenarios" => { + run_build( + "debug_build", + &[ + "run", + "--manifest-path", + "component_integration_tests/rust_test_scenarios/Cargo.toml", + "--bin", + "rust_test_scenarios", + ], + envs, + cli_env_vars, + &passthrough_args, + ); + } + _ => print_usage_and_exit(), + } +} + +fn clippy(envs: HashMap, cli_env_vars: HashMap, passthrough_args: &[String]) { + run_build( + "clippy", + &["clippy", "--all-targets", "--all-features"], + envs, + cli_env_vars, + passthrough_args, + ); +} + +fn test(envs: HashMap, cli_env_vars: HashMap, passthrough_args: &[String]) { + run_build("test_build", &["test"], envs, cli_env_vars, passthrough_args); +} + +fn debug_build(envs: HashMap, cli_env_vars: HashMap, passthrough_args: &[String]) { + run_build("debug_build", &["build"], envs, cli_env_vars, passthrough_args); +} + +fn run_build( + target_dir: &str, + cargo_args: &[&str], + mut default_envs: HashMap, + cli_envs: HashMap, + extra_args: &[String], +) { + // Set target dir + default_envs.insert("CARGO_TARGET_DIR".into(), format!("target/{}", target_dir)); + + // CLI overrides + for (k, v) in cli_envs { + default_envs.insert(k, v); + } + + run_command(cargo_args, default_envs, extra_args, None); +} + +fn run_command(cargo_args: &[&str], default_envs: HashMap, extra_args: &[String], explain: Option<&str>) { + let mut cmd = Command::new("cargo"); + cmd.args(cargo_args); + cmd.args(extra_args); + + for (key, value) in &default_envs { + cmd.env(key, value); + } + + println!("> Running: cargo {} {}", cargo_args.join(" "), extra_args.join(" ")); + println!("> With envs: {:?}", default_envs); + + let status = cmd.status().unwrap_or_else(|_| panic!("Failed to run cargo with explain {:?}", explain)); + if !status.success() { + exit(status.code().unwrap_or(1)); + } +} + +fn print_usage_and_exit() -> ! { + eprintln!( + "Usage: xtask {{ + build build in debug mode + run runs executable + build:release build in release mode + run:release runs executable in release mode + build:test build and runs tests + build:loom builds and runs loom tests only + build:qnx_x86_64 build for QNX7.1 target: x86_64-pc-nto-qnx710 + build:qnx_arm build for QNX7.1 target: aarch64-pc-nto-qnx710 + clippy runs clippy + check runs fundamental checks, good to run before push + check_lic runs source code license check + + [ENV_VAR=value ...] [-- cargo args...]" + ); + exit(1); +} + +const REQUIRED_HEADER: &str = r#"// Copyright (c) 2025 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +//"#; + +fn check_license_header() { + let project_dir = std::env::current_dir().expect("Failed to get current directory").join("src"); + let mut missing_header_files = Vec::new(); + + visit_dirs(&project_dir, &mut missing_header_files); + + if missing_header_files.is_empty() { + println!("All files have the required license header."); + } else { + println!("The following files are missing the required license header:"); + println!("\n{}\n", REQUIRED_HEADER); + for file in missing_header_files { + println!("{}", file.display()); + } + + std::process::exit(-1); + } +} + +fn visit_dirs(dir: &Path, missing_header_files: &mut Vec) { + if dir.is_dir() { + for entry in fs::read_dir(dir).expect("Failed to read directory") { + let entry = entry.expect("Failed to get directory entry"); + let path = entry.path(); + if path.is_dir() { + visit_dirs(&path, missing_header_files); + } else if path.extension().is_some_and(|ext| ext == "rs") { + check_file(&path, missing_header_files); + } + } + } +} + +fn check_file(file_path: &Path, missing_header_files: &mut Vec) { + let content = fs::read_to_string(file_path).expect("Failed to read file"); + if !content.starts_with(REQUIRED_HEADER) { + missing_header_files.push(file_path.to_path_buf()); + } +} diff --git a/tests/fault_catalog.json b/tests/fault_catalog.json new file mode 100644 index 0000000..29a5b95 --- /dev/null +++ b/tests/fault_catalog.json @@ -0,0 +1,53 @@ +{ + "id": "hvac", + "version": 3, + "faults": [ + { + "id": { + "Text": "d1" + }, + "name": "Descriptor 1", + "summary": null, + "category": "Software", + "severity": "Debug", + "compliance": [ + "EmissionRelevant", + "SafetyCritical" + ], + "reporter_side_debounce": { + "EdgeWithCooldown": { + "cooldown": { + "secs": 0, + "nanos": 100000000 + } + } + }, + "reporter_side_reset": null, + "manager_side_debounce": null, + "manager_side_reset": null + }, + { + "id": { + "Text": "d2" + }, + "name": "Descriptor 2", + "summary": "Human-readable summary", + "category": "Configuration", + "severity": "Warn", + "compliance": [ + "SecurityRelevant" + ], + "reporter_side_debounce": null, + "reporter_side_reset": null, + "manager_side_debounce": { + "EdgeWithCooldown": { + "cooldown": { + "secs": 0, + "nanos": 100000000 + } + } + }, + "manager_side_reset": null + } + ] +} \ No newline at end of file