Skip to content

File functional_safety_advanced.hpp

File List > diagnostics > functional_safety_advanced.hpp

Go to the documentation of this file

// Copyright 2025 TIER IV, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#pragma once

#include "nebula_decoders/nebula_decoders_hesai/decoders/functional_safety.hpp"
#include "nebula_ros/hesai/diagnostics/functional_safety_diagnostic_task.hpp"

#include <diagnostic_msgs/msg/detail/key_value__struct.hpp>

#include <boost/algorithm/string.hpp>
#include <boost/range/algorithm/find.hpp>
#include <boost/range/algorithm/find_if.hpp>
#include <boost/tokenizer.hpp>

#include <algorithm>
#include <cstdint>
#include <fstream>
#include <iomanip>
#include <limits>
#include <sstream>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>

namespace nebula::ros
{

using std::string_literals::operator""s;

struct ErrorDefinition
{
  uint16_t code;
  std::string description;
  drivers::FunctionalSafetySeverity severity;

  bool operator==(const ErrorDefinition & other) const
  {
    return code == other.code && description == other.description && severity == other.severity;
  }

  friend std::ostream & operator<<(std::ostream & os, const ErrorDefinition & error_definition)
  {
    os << "0x" << std::hex << std::setw(4) << std::setfill('0') << error_definition.code << " ("
       << error_definition.severity << ")";
    return os;
  }
};

inline drivers::FunctionalSafetySeverity string_to_severity(const std::string & severity_str)
{
  std::string lower_str = boost::algorithm::to_lower_copy(severity_str);

  if (lower_str == "ok") {
    return drivers::FunctionalSafetySeverity::OK;
  }

  if (lower_str == "warning") {
    return drivers::FunctionalSafetySeverity::WARNING;
  }

  if (lower_str == "error") {
    return drivers::FunctionalSafetySeverity::ERROR;
  }

  throw std::runtime_error("Unknown severity level: " + severity_str);
}

inline std::vector<ErrorDefinition> read_error_definitions_from_csv(const std::string & file_path)
{
  constexpr int hex_base = 16;
  std::ifstream file(file_path);

  if (!file.is_open()) {
    throw std::runtime_error("Failed to open CSV file: " + file_path);
  }

  std::string line;

  auto make_exception = [&line](const std::string & reason) {
    return std::runtime_error("Failed to parse line (" + reason + "): " + line);
  };

  bool got_header = false;
  // Track definitions by code to detect and reconcile duplicates
  std::unordered_map<uint16_t, ErrorDefinition> parsed_definitions_by_code;
  while (std::getline(file, line)) {
    if (line.empty()) {
      continue;
    }

    // Use boost::tokenizer to split the CSV line on semicolons
    boost::escaped_list_separator<char> separator('\\', ';', '"');
    boost::tokenizer<boost::escaped_list_separator<char>> tokenizer(line, separator);

    std::vector<std::string> fields;
    for (const auto & token : tokenizer) {
      fields.push_back(token);
    }

    // Validate that we have exactly 3 fields
    if (fields.size() < 3) {
      throw make_exception("expected at least 3 fields (code;description;severity)");
    }

    if (
      !got_header &&
      (fields[0] == "Fault Code" && fields[1] == "Fault" && fields[2] == "Severity")) {
      got_header = true;
      continue;
    }

    // Parse fault code
    const std::string & code_field = fields[0];
    uint16_t code = 0;
    if (code_field.size() < 2 || code_field.substr(0, 2) != "0x") {
      throw make_exception("expected fault code of shape 0xABCD");
    }

    {
      const uint64_t parsed = std::stoul(code_field.substr(2), nullptr, hex_base);
      if (parsed > std::numeric_limits<uint16_t>::max()) {
        throw make_exception("fault code does not fit in uint16");
      }
      code = static_cast<uint16_t>(parsed);
    }

    // Parse description and severity
    const std::string & description = fields[1];
    const std::string & severity_str = fields[2];
    drivers::FunctionalSafetySeverity severity = string_to_severity(severity_str);

    const ErrorDefinition current{code, description, severity};

    const auto it = parsed_definitions_by_code.find(code);
    if (it == parsed_definitions_by_code.end()) {
      parsed_definitions_by_code.emplace(code, current);
      continue;
    }

    // Everything beyond this point is a duplicate code.

    // If it is a 1:1 duplicate, ignore it.
    if (it->second == current) {
      continue;
    }

    // Combine descriptions of duplicate definitions, and take the worst severity.
    it->second.description = it->second.description + " OR " + current.description;
    it->second.severity = std::max(it->second.severity, current.severity);
  }

  std::vector<ErrorDefinition> error_definitions;
  error_definitions.reserve(parsed_definitions_by_code.size());
  for (const auto & [code, definition] : parsed_definitions_by_code) {
    error_definitions.push_back(definition);
  }
  return error_definitions;
}

class FunctionalSafetyAdvanced : public FunctionalSafetyStatusProcessor
{
public:
  FunctionalSafetyAdvanced(
    const std::vector<ErrorDefinition> & error_definitions,
    const std::vector<uint16_t> & exempted_codes)
  : exempted_codes_(exempted_codes.begin(), exempted_codes.end())
  {
    for (const auto & error_definition : error_definitions) {
      error_definitions_.emplace(error_definition.code, error_definition);
    }
  }

  void populate_status(
    drivers::FunctionalSafetySeverity severity,
    const drivers::FunctionalSafetyErrorCodes & error_codes,
    diagnostic_msgs::msg::DiagnosticStatus & inout_status) override
  {
    diagnostic_msgs::msg::KeyValue kv;
    kv.key = "Diagnostic codes";
    kv.value = detail::error_codes_to_string(error_codes);
    inout_status.values.push_back(kv);

    if (error_codes.empty()) {
      // If there are no error codes, report the severity reported by the sensor
      inout_status.level = detail::severity_to_diagnostic_status_level(severity);
      inout_status.message = detail::status_to_string(severity, 0);
      return;
    }

    auto [non_exempted_error_codes, exempted_error_codes] =
      split_error_codes(error_codes, exempted_codes_);

    drivers::FunctionalSafetySeverity max_severity = drivers::FunctionalSafetySeverity::OK;
    for (const auto & error_code : non_exempted_error_codes) {
      // If the error code is not found in the definitions, use the severity reported by the
      // sensor as the worst-case assumption.
      const auto error_definition =
        get_error_definition(error_code)
          .value_or(ErrorDefinition{error_code, "Unknown error", severity});

      max_severity = std::max(max_severity, error_definition.severity);
      auto error_kv = to_key_value(error_definition);
      inout_status.values.push_back(error_kv);
    }

    for (const auto & error_code : exempted_error_codes) {
      const auto error_definition =
        get_error_definition(error_code)
          .value_or(ErrorDefinition{error_code, "Unknown error", severity});
      auto error_kv = to_key_value(error_definition);
      error_kv.value = "[Exempted] " + error_kv.value;
      inout_status.values.push_back(error_kv);
    }

    inout_status.level = detail::severity_to_diagnostic_status_level(max_severity);

    // When the effective severity is OK, report nominal operation regardless of code count
    const size_t n_errors_for_message =
      (max_severity == drivers::FunctionalSafetySeverity::OK) ? 0 : non_exempted_error_codes.size();
    inout_status.message = detail::status_to_string(max_severity, n_errors_for_message);
  }

private:
  static std::tuple<drivers::FunctionalSafetyErrorCodes, drivers::FunctionalSafetyErrorCodes>
  split_error_codes(
    const drivers::FunctionalSafetyErrorCodes & error_codes,
    const std::unordered_set<uint16_t> & exempted_codes)
  {
    drivers::FunctionalSafetyErrorCodes non_exempted_error_codes;
    drivers::FunctionalSafetyErrorCodes exempted_error_codes;
    for (const auto & error_code : error_codes) {
      if (boost::range::find(exempted_codes, error_code) == exempted_codes.end()) {
        non_exempted_error_codes.push_back(error_code);
      } else {
        exempted_error_codes.push_back(error_code);
      }
    }
    return std::make_tuple(non_exempted_error_codes, exempted_error_codes);
  }

  [[nodiscard]] std::optional<ErrorDefinition> get_error_definition(uint16_t error_code) const
  {
    auto it = error_definitions_.find(error_code);

    if (it == error_definitions_.end()) {
      return std::nullopt;
    }

    return it->second;
  }

  static diagnostic_msgs::msg::KeyValue to_key_value(const ErrorDefinition & error_definition)
  {
    diagnostic_msgs::msg::KeyValue kv;
    std::stringstream ss;
    ss << "0x" << std::hex << std::setw(4) << std::setfill('0') << error_definition.code;
    kv.key = ss.str();

    char prefix{};
    switch (error_definition.severity) {
      case drivers::FunctionalSafetySeverity::OK:
        prefix = 'O';
        break;
      case drivers::FunctionalSafetySeverity::WARNING:
        prefix = 'W';
        break;
      case drivers::FunctionalSafetySeverity::ERROR:
        prefix = 'E';
        break;
      default:
        throw std::runtime_error(
          "Unknown severity level: " + std::to_string(static_cast<int>(error_definition.severity)));
    }
    kv.value = "["s + prefix + "] " + error_definition.description;
    return kv;
  }

  std::unordered_map<uint16_t, ErrorDefinition> error_definitions_;
  std::unordered_set<uint16_t> exempted_codes_;
};

}  // namespace nebula::ros