diff --git a/dbc.pro b/dbc.pro new file mode 100644 index 0000000..b646ea7 --- /dev/null +++ b/dbc.pro @@ -0,0 +1,18 @@ +TEMPLATE = app +CONFIG += console c++17 +CONFIG -= app_bundle +CONFIG -= qt + +SOURCES += \ + dbc_parser.cpp \ + dbc_tree_builder.cpp \ + main.cpp \ + tree_node.cpp + +HEADERS += \ + dbc_database.h \ + dbc_parser.h \ + dbc_tree_builder.h \ + frame_info.h \ + signal_info.h \ + tree_node.h diff --git a/dbc_database.h b/dbc_database.h new file mode 100644 index 0000000..ba52339 --- /dev/null +++ b/dbc_database.h @@ -0,0 +1,15 @@ +#ifndef DBC_DATABASE_H +#define DBC_DATABASE_H + +#include + +#include "frame_info.h" + +/** + * @brief Parsed DBC content stored in a simple internal form. + */ +struct DbcDatabase { + std::vector frames; /**< All frames found in DBC file. */ +}; + +#endif /* DBC_DATABASE_H */ diff --git a/dbc_parser.cpp b/dbc_parser.cpp new file mode 100644 index 0000000..a039312 --- /dev/null +++ b/dbc_parser.cpp @@ -0,0 +1,315 @@ +#include "dbc_parser.h" + +#include +#include +#include +#include +#include + +namespace { + /** + * @brief Remove leading and trailing spaces. + * @param text Input text. + * @return Trimmed text. + */ + std::string TrimText (const std::string &text) { + std::string::size_type begin = 0U; + while ((begin < text.size()) && std::isspace (static_cast (text[begin]))) + ++begin; + + std::string::size_type end = text.size(); + while ((end > begin) && std::isspace (static_cast (text[end - 1U]))) + --end; + + return text.substr (begin, end - begin); + } +} + +DbcDatabase DbcParser::ParseFile (const std::string &filePath) const { + std::ifstream input (filePath.c_str()); + if (!input.is_open()) + throw std::runtime_error ("Failed to open DBC file: " + filePath); + + DbcDatabase database; + FrameInfo *currentFrame = nullptr; + + std::string line; + while (std::getline (input, line)) { + line = Trim (line); + if (line.empty()) + continue; + + if (IsFrameLine (line)) { + FrameInfo frame = ParseFrameLine (line); + database.frames.push_back (frame); + currentFrame = &database.frames.back(); + } else if (IsSignalLine (line)) { + if (currentFrame == nullptr) + throw std::runtime_error ("Signal found before any frame definition."); + + SignalInfo signal = ParseSignalLine (line); + currentFrame->signals.push_back (signal); + } else if (IsCommentLine (line)) + ParseCommentLine (line, database); + } + + return database; +} + +bool DbcParser::IsFrameLine (const std::string &line) { + return (line.size() >= 4U) && (line.compare (0U, 4U, "BO_ ") == 0); +} + +bool DbcParser::IsSignalLine (const std::string &line) { + return (line.size() >= 4U) && (line.compare (0U, 4U, "SG_ ") == 0); +} + +bool DbcParser::IsCommentLine (const std::string &line) { + return (line.size() >= 4U) && (line.compare (0U, 4U, "CM_ ") == 0); +} + +std::string DbcParser::Trim (const std::string &text) { + return TrimText (text); +} + +std::vector DbcParser::SplitReceivers (const std::string &text) { + std::vector receivers; + std::string token; + std::istringstream stream (text); + + while (std::getline (stream, token, ',')) { + token = TrimText (token); + if (!token.empty()) + receivers.push_back (token); + } + + return receivers; +} + +std::uint32_t DbcParser::TryExtractPgn (std::uint32_t canId, bool &hasPgn) { + hasPgn = false; + + /* + * Very simplified J1939 PGN extraction. + * Assumes 29-bit identifier. + */ + if ((canId & 0x1FFFFFFFU) != canId) + return 0U; + + const std::uint32_t pf = (canId >> 16U) & 0xFFU; + const std::uint32_t ps = (canId >> 8U) & 0xFFU; + const std::uint32_t dp = (canId >> 24U) & 0x01U; + + std::uint32_t pgn = 0U; + + if (pf < 240U) + pgn = (dp << 16U) | (pf << 8U); + else + pgn = (dp << 16U) | (pf << 8U) | ps; + + hasPgn = true; + return pgn; +} + +FrameInfo DbcParser::ParseFrameLine (const std::string &line) { + /* + * Example: + * BO_ 256 EngineData: 8 Vector__XXX + */ + + std::istringstream stream (line); + std::string token; + FrameInfo frame; + + stream >> token; + if (token != "BO_") + throw std::runtime_error ("Invalid frame line: " + line); + + stream >> frame.canId; + stream >> token; + + if (token.empty()) + throw std::runtime_error ("Missing frame name: " + line); + + if (token[token.size() - 1U] == ':') + token.erase (token.size() - 1U, 1U); + + frame.name = token; + + { + unsigned int dlcValue = 0U; + stream >> dlcValue; + frame.dlc = static_cast (dlcValue); + } + + stream >> frame.transmitter; + frame.pgn = TryExtractPgn (frame.canId, frame.hasPgn); + + return frame; +} + +SignalInfo DbcParser::ParseSignalLine (const std::string &line) { + /* + * Example: + * SG_ EngineSpeed : 0|16@1+ (0.125,0) [0|8000] "rpm" Vector__XXX + */ + + SignalInfo signal; + + std::string work = TrimText (line); + if (work.compare (0U, 4U, "SG_ ") != 0) + throw std::runtime_error ("Invalid signal line: " + line); + + work.erase (0U, 4U); + + const std::string::size_type colonPos = work.find (':'); + if (colonPos == std::string::npos) + throw std::runtime_error ("Signal line missing ':' : " + line); + + signal.name = TrimText (work.substr (0U, colonPos)); + std::string rest = TrimText (work.substr (colonPos + 1U)); + + const std::string::size_type pipePos = rest.find ('|'); + const std::string::size_type atPos = rest.find ('@'); + const std::string::size_type signPos = rest.find_first_of ("+-", atPos); + const std::string::size_type factorBegin = rest.find ('('); + const std::string::size_type factorComma = rest.find (',', factorBegin); + const std::string::size_type factorEnd = rest.find (')', factorComma); + const std::string::size_type rangeBegin = rest.find ('['); + const std::string::size_type rangeSep = rest.find ('|', rangeBegin); + const std::string::size_type rangeEnd = rest.find (']', rangeSep); + const std::string::size_type unitBegin = rest.find ('"', rangeEnd); + const std::string::size_type unitEnd = rest.find ('"', unitBegin + 1U); + + if ((pipePos == std::string::npos) || + (atPos == std::string::npos) || + (signPos == std::string::npos) || + (factorBegin == std::string::npos) || + (factorComma == std::string::npos) || + (factorEnd == std::string::npos) || + (rangeBegin == std::string::npos) || + (rangeSep == std::string::npos) || + (rangeEnd == std::string::npos) || + (unitBegin == std::string::npos) || + (unitEnd == std::string::npos)) + throw std::runtime_error ("Unsupported signal syntax: " + line); + + signal.startBit = static_cast ( + std::stoul (TrimText (rest.substr (0U, pipePos))) + ); + + signal.length = static_cast ( + std::stoul (TrimText (rest.substr (pipePos + 1U, atPos - pipePos - 1U))) + ); + + { + if ((atPos + 1U) >= rest.size()) + throw std::runtime_error ("Invalid endianness in signal: " + line); + + const char endianChar = rest[atPos + 1U]; + signal.isLittleEndian = (endianChar == '1'); + } + + { + const char signChar = rest[signPos]; + signal.isSigned = (signChar == '-'); + } + + signal.factor = std::stod ( + TrimText (rest.substr (factorBegin + 1U, factorComma - factorBegin - 1U)) + ); + + signal.offset = std::stod ( + TrimText (rest.substr (factorComma + 1U, factorEnd - factorComma - 1U)) + ); + + signal.minimum = std::stod ( + TrimText (rest.substr (rangeBegin + 1U, rangeSep - rangeBegin - 1U)) + ); + + signal.maximum = std::stod ( + TrimText (rest.substr (rangeSep + 1U, rangeEnd - rangeSep - 1U)) + ); + + signal.unit = rest.substr (unitBegin + 1U, unitEnd - unitBegin - 1U); + + { + const std::string receiversText = TrimText (rest.substr (unitEnd + 1U)); + signal.receivers = SplitReceivers (receiversText); + } + + return signal; +} + +void DbcParser::ParseCommentLine (const std::string &line, DbcDatabase &database) { + /* + * Examples: + * CM_ BO_ 256 "Frame comment"; + * CM_ SG_ 256 EngineSpeed "Signal comment"; + */ + + std::istringstream stream (line); + std::string token; + stream >> token; + + if (token != "CM_") + return; + + stream >> token; + + if (token == "BO_") { + std::uint32_t canId = 0U; + stream >> canId; + + const std::string::size_type quoteBegin = line.find ('"'); + const std::string::size_type quoteEnd = line.rfind ('"'); + + if ((quoteBegin == std::string::npos) || + (quoteEnd == std::string::npos) || + (quoteEnd <= quoteBegin)) + return; + + FrameInfo *frame = FindFrameById (database, canId); + if (frame != nullptr) + frame->comment = line.substr (quoteBegin + 1U, quoteEnd - quoteBegin - 1U); + } else if (token == "SG_") { + std::uint32_t canId = 0U; + std::string signalName; + + stream >> canId; + stream >> signalName; + + const std::string::size_type quoteBegin = line.find ('"'); + const std::string::size_type quoteEnd = line.rfind ('"'); + + if ((quoteBegin == std::string::npos) || + (quoteEnd == std::string::npos) || + (quoteEnd <= quoteBegin)) + return; + + FrameInfo *frame = FindFrameById (database, canId); + if (frame != nullptr) { + SignalInfo *signal = FindSignalByName (*frame, signalName); + if (signal != nullptr) + signal->comment = line.substr (quoteBegin + 1U, quoteEnd - quoteBegin - 1U); + } + } +} + +FrameInfo *DbcParser::FindFrameById (DbcDatabase &database, std::uint32_t canId) { + for (std::size_t index = 0U; index < database.frames.size(); ++index) { + if (database.frames[index].canId == canId) + return &database.frames[index]; + } + + return nullptr; +} + +SignalInfo *DbcParser::FindSignalByName (FrameInfo &frame, const std::string &signalName) { + for (std::size_t index = 0U; index < frame.signals.size(); ++index) { + if (frame.signals[index].name == signalName) + return &frame.signals[index]; + } + + return nullptr; +} diff --git a/dbc_parser.h b/dbc_parser.h new file mode 100644 index 0000000..8e0c0c7 --- /dev/null +++ b/dbc_parser.h @@ -0,0 +1,51 @@ +#ifndef DBC_PARSER_H +#define DBC_PARSER_H + +#include +#include +#include + +#include "dbc_database.h" + +/** + * @brief Minimal DBC parser. + * + * Supports: + * - BO_ + * - SG_ + * - CM_ BO_ + * - CM_ SG_ + * + * Ignores: + * - attributes + * - multiplexing + * - value tables + */ +class DbcParser { + public: + /** + * @brief Parse DBC file. + * @param filePath Path to input file. + * @return Parsed database. + * @throws std::runtime_error on file or parse errors. + */ + DbcDatabase ParseFile (const std::string &filePath) const; + + private: + static bool IsFrameLine (const std::string &line); + static bool IsSignalLine (const std::string &line); + static bool IsCommentLine (const std::string &line); + static std::string Trim (const std::string &text); + static std::vector SplitReceivers (const std::string &text); + static std::uint32_t TryExtractPgn (std::uint32_t canId, bool &hasPgn); + + static FrameInfo ParseFrameLine (const std::string &line); + static SignalInfo ParseSignalLine (const std::string &line); + + static void ParseCommentLine (const std::string &line, DbcDatabase &database); + + static FrameInfo *FindFrameById (DbcDatabase &database, std::uint32_t canId); + static SignalInfo *FindSignalByName (FrameInfo &frame, const std::string &signalName); +}; + +#endif /* DBC_PARSER_H */ diff --git a/dbc_tree_builder.cpp b/dbc_tree_builder.cpp new file mode 100644 index 0000000..c4e5528 --- /dev/null +++ b/dbc_tree_builder.cpp @@ -0,0 +1,20 @@ +#include "dbc_tree_builder.h" + +std::unique_ptr DbcTreeBuilder::Build (const DbcDatabase &database) const { + std::unique_ptr root (new TreeNode()); + + for (std::size_t frameIndex = 0U; frameIndex < database.frames.size(); ++frameIndex) { + const FrameInfo &frame = database.frames[frameIndex]; + std::unique_ptr frameNode (new TreeNode (frame)); + + for (std::size_t signalIndex = 0U; signalIndex < frame.signals.size(); ++signalIndex) { + const SignalInfo &signal = frame.signals[signalIndex]; + std::unique_ptr signalNode (new TreeNode (signal)); + frameNode->AddChild (std::move (signalNode)); + } + + root->AddChild (std::move (frameNode)); + } + + return root; +} diff --git a/dbc_tree_builder.h b/dbc_tree_builder.h new file mode 100644 index 0000000..931c181 --- /dev/null +++ b/dbc_tree_builder.h @@ -0,0 +1,22 @@ +#ifndef DBC_TREE_BUILDER_H +#define DBC_TREE_BUILDER_H + +#include + +#include "dbc_database.h" +#include "tree_node.h" + +/** + * @brief Builds a simple tree from parsed DBC database. + */ +class DbcTreeBuilder { + public: + /** + * @brief Build tree representation of parsed DBC data. + * @param database Parsed database. + * @return Root node of the tree. + */ + std::unique_ptr Build (const DbcDatabase &database) const; +}; + +#endif /* DBC_TREE_BUILDER_H */ diff --git a/frame_info.h b/frame_info.h new file mode 100644 index 0000000..7f24ed9 --- /dev/null +++ b/frame_info.h @@ -0,0 +1,35 @@ +#ifndef FRAME_INFO_H +#define FRAME_INFO_H + +#include +#include +#include + +#include "signal_info.h" + +/** + * @brief Describes one CAN frame from a DBC file. + */ +struct FrameInfo { + std::string name; /**< Frame name. */ + std::uint32_t canId; /**< Raw CAN identifier from DBC. */ + std::uint32_t pgn; /**< J1939 PGN if applicable. */ + bool hasPgn; /**< true if PGN was derived. */ + std::uint8_t dlc; /**< Frame payload length. */ + std::string transmitter; /**< Transmitter ECU name. */ + std::string comment; /**< Optional frame comment. */ + std::vector signals; /**< Signals contained in the frame. */ + + FrameInfo() + : name() + , canId (0U) + , pgn (0U) + , hasPgn (false) + , dlc (0U) + , transmitter() + , comment() + , signals() { + } +}; + +#endif /* FRAME_INFO_H */ diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..4ed5c3a --- /dev/null +++ b/main.cpp @@ -0,0 +1,99 @@ +#include +#include +#include + +#include "dbc_parser.h" +#include "dbc_tree_builder.h" +#include "tree_node.h" + +static void PrintReceivers (const std::vector &receivers) { + for (std::size_t index = 0U; index < receivers.size(); ++index) { + if (index != 0U) + std::cout << ","; + + std::cout << receivers[index]; + } +} + +static void PrintTree (const TreeNode *node, int indent) { + if (node == nullptr) + return; + + for (int i = 0; i < indent; ++i) + std::cout << " "; + + switch (node->GetType()) { + case NodeType::Root: + std::cout << "[root] " << node->GetName() << "\n"; + break; + + case NodeType::Frame: { + const FrameInfo *frame = node->GetFrame(); + std::cout << "[frame] " << node->GetName(); + + if (frame != nullptr) { + std::cout << " id=" << frame->canId + << " dlc=" << static_cast (frame->dlc) + << " tx=" << frame->transmitter; + + if (frame->hasPgn) + std::cout << " pgn=" << frame->pgn; + + if (!frame->comment.empty()) + std::cout << " comment=\"" << frame->comment << "\""; + } + + std::cout << "\n"; + break; + } + + case NodeType::Signal: { + const SignalInfo *signal = node->GetSignal(); + std::cout << "[signal] " << node->GetName(); + + if (signal != nullptr) { + std::cout << " start=" << signal->startBit + << " len=" << signal->length + << " unit=" << signal->unit + << " rx="; + + PrintReceivers (signal->receivers); + + if (!signal->comment.empty()) + std::cout << " comment=\"" << signal->comment << "\""; + } + + std::cout << "\n"; + break; + } + + default: + std::cout << "[unknown]\n"; + break; + } + + for (std::size_t i = 0U; i < node->GetChildCount(); ++i) + PrintTree (node->GetChild (i), indent + 1); +} + +int main (int argc, char *argv[]) { + if (argc < 2) { + std::cerr << "Usage: dbc_demo \n"; + return 1; + } + + try { + DbcParser parser; + DbcDatabase database = parser.ParseFile (argv[1]); + + DbcTreeBuilder builder; + std::unique_ptr root = builder.Build (database); + + PrintTree (root.get(), 0); + } catch (const std::exception &ex) { + std::cerr << "Error: " << ex.what() << "\n"; + return 2; + } + + return 0; +} diff --git a/signal_info.h b/signal_info.h new file mode 100644 index 0000000..55737bc --- /dev/null +++ b/signal_info.h @@ -0,0 +1,41 @@ +#ifndef SIGNAL_INFO_H +#define SIGNAL_INFO_H + +#include +#include +#include + +/** + * @brief Describes one signal inside a DBC frame. + */ +struct SignalInfo { + std::string name; /**< Signal name. */ + std::uint32_t startBit; /**< Start bit in DBC notation. */ + std::uint32_t length; /**< Signal length in bits. */ + bool isLittleEndian; /**< true for Intel, false for Motorola. */ + bool isSigned; /**< true if signal is signed. */ + double factor; /**< Scaling factor. */ + double offset; /**< Physical offset. */ + double minimum; /**< Minimum physical value. */ + double maximum; /**< Maximum physical value. */ + std::string unit; /**< Physical unit. */ + std::vector receivers; /**< Receivers of this signal. */ + std::string comment; /**< Optional signal comment. */ + + SignalInfo() + : name() + , startBit (0U) + , length (0U) + , isLittleEndian (true) + , isSigned (false) + , factor (1.0) + , offset (0.0) + , minimum (0.0) + , maximum (0.0) + , unit() + , receivers() + , comment() { + } +}; + +#endif /* SIGNAL_INFO_H */ diff --git a/tree_node.cpp b/tree_node.cpp new file mode 100644 index 0000000..d438243 --- /dev/null +++ b/tree_node.cpp @@ -0,0 +1,64 @@ +#include "tree_node.h" + +TreeNode::TreeNode() + : m_type (NodeType::Root) + , m_name ("dbc") + , m_children() + , m_frame() + , m_signal() { +} + +TreeNode::TreeNode (const FrameInfo &frame) + : m_type (NodeType::Frame) + , m_name (frame.name) + , m_children() + , m_frame (new FrameInfo (frame)) + , m_signal() { +} + +TreeNode::TreeNode (const SignalInfo &signal) + : m_type (NodeType::Signal) + , m_name (signal.name) + , m_children() + , m_frame() + , m_signal (new SignalInfo (signal)) { +} + +void TreeNode::AddChild (std::unique_ptr child) { + if (child) + m_children.push_back (std::move (child)); +} + +std::size_t TreeNode::GetChildCount() const { + return m_children.size(); +} + +const TreeNode *TreeNode::GetChild (std::size_t index) const { + if (index >= m_children.size()) + return nullptr; + + return m_children[index].get(); +} + +TreeNode *TreeNode::GetChild (std::size_t index) { + if (index >= m_children.size()) + return nullptr; + + return m_children[index].get(); +} + +NodeType TreeNode::GetType() const { + return m_type; +} + +const std::string &TreeNode::GetName() const { + return m_name; +} + +const FrameInfo *TreeNode::GetFrame() const { + return m_frame.get(); +} + +const SignalInfo *TreeNode::GetSignal() const { + return m_signal.get(); +} diff --git a/tree_node.h b/tree_node.h new file mode 100644 index 0000000..3015db4 --- /dev/null +++ b/tree_node.h @@ -0,0 +1,109 @@ +#ifndef TREE_NODE_H +#define TREE_NODE_H + +#include +#include +#include +#include + +#include "frame_info.h" +#include "signal_info.h" + +/** + * @brief Type of a tree node. + */ +enum class NodeType { + Root, + Frame, + Signal +}; + +/** + * @brief Tree node for later use in model/view or other hierarchy consumers. + */ +class TreeNode { + public: + /** + * @brief Create root node. + */ + TreeNode(); + + /** + * @brief Create frame node. + * @param frame Frame payload. + */ + explicit TreeNode (const FrameInfo &frame); + + /** + * @brief Create signal node. + * @param signal Signal payload. + */ + explicit TreeNode (const SignalInfo &signal); + + ~TreeNode() = default; + + TreeNode (const TreeNode &) = delete; + TreeNode &operator= (const TreeNode &) = delete; + + TreeNode (TreeNode &&) = default; + TreeNode &operator= (TreeNode &&) = default; + + /** + * @brief Add child node. + * @param child Child node to add. + */ + void AddChild (std::unique_ptr child); + + /** + * @brief Get child count. + * @return Number of children. + */ + std::size_t GetChildCount() const; + + /** + * @brief Get child by index. + * @param index Child index. + * @return Child pointer or nullptr if index is invalid. + */ + const TreeNode *GetChild (std::size_t index) const; + + /** + * @brief Get mutable child by index. + * @param index Child index. + * @return Child pointer or nullptr if index is invalid. + */ + TreeNode *GetChild (std::size_t index); + + /** + * @brief Get node type. + * @return Node type. + */ + NodeType GetType() const; + + /** + * @brief Get display name. + * @return Node name. + */ + const std::string &GetName() const; + + /** + * @brief Get frame payload if node is frame. + * @return Pointer to frame info or nullptr. + */ + const FrameInfo *GetFrame() const; + + /** + * @brief Get signal payload if node is signal. + * @return Pointer to signal info or nullptr. + */ + const SignalInfo *GetSignal() const; + + private: + NodeType m_type; + std::string m_name; + std::vector > m_children; + std::unique_ptr m_frame; + std::unique_ptr m_signal; +}; + +#endif /* TREE_NODE_H */