Class: CanMessenger::DBC

Inherits:
Object
  • Object
show all
Defined in:
lib/can_messenger/dbc.rb

Overview

DBC (Database CAN) Parser and Encoder/Decoder

This class provides functionality to parse DBC files and encode/decode CAN messages according to the signal definitions. DBC files are a standard way to describe CAN network communication.

Examples:

Loading and using a DBC file

dbc = CanMessenger::DBC.load('vehicle.dbc')

# Encode a message with signal values
frame = dbc.encode_can('EngineData', RPM: 2500, Temperature: 85.5)
# => { id: 0x123, data: [0x09, 0xC4, 0xAB, 0x00, 0x00, 0x00, 0x00, 0x00] }

# Decode a received CAN frame
decoded = dbc.decode_can(0x123, [0x09, 0xC4, 0xAB, 0x00, 0x00, 0x00, 0x00, 0x00])
# => { name: 'EngineData', signals: { RPM: 2500.0, Temperature: 85.5 } }

Constant Summary collapse

IGNORED_LINE_PREFIXES =
%w[
  BO_TX_BU_
  VERSION
  NS_
  BS_
  BU_
  CM_
  VAL_
  BA_
  BA_DEF_
  BA_DEF_DEF_
  SIG_VALTYPE_
].freeze
MESSAGE_LINE_PATTERN =
/^BO_\s+(\d+)\s+(\w+)\s*:\s*(\d+)\s+\S.*$/
SIGNAL_LINE_PATTERN =
/^SG_\s+(\w+)\s*:\s*(\d+)\|(\d+)@(\d)([+-])\s*\(([^,]+),([^)]+)\)/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(content = "") ⇒ DBC

Initializes a new DBC instance.

Parameters:

  • content (String) (defaults to: "")

    The DBC file content to parse (optional)



53
54
55
56
# File 'lib/can_messenger/dbc.rb', line 53

def initialize(content = "")
  @messages = {}
  parse(content) unless content.empty?
end

Instance Attribute Details

#messagesObject (readonly)

Returns the value of attribute messages.



38
39
40
# File 'lib/can_messenger/dbc.rb', line 38

def messages
  @messages
end

Class Method Details

.load(path) ⇒ DBC

Loads a DBC file from disk and parses its contents.

Parameters:

  • path (String)

    The filesystem path to the DBC file

Returns:

  • (DBC)

    A new DBC instance with parsed message definitions

Raises:

  • (Errno::ENOENT)

    If the file doesn’t exist

  • (ArgumentError)

    If the file contains invalid DBC syntax



46
47
48
# File 'lib/can_messenger/dbc.rb', line 46

def self.load(path)
  new(File.read(path))
end

Instance Method Details

#decode_can(id, data) ⇒ Hash?

Decodes a CAN message frame into signal values.

Takes a CAN ID and data bytes, finds the matching message definition, and decodes the data into individual signal values according to the DBC.

Examples:

decoded = dbc.decode_can(0x123, [0x09, 0xC4, 0xAB, 0x00, 0x00, 0x00, 0x00, 0x00])
# => { name: 'EngineData', signals: { RPM: 2500.0, Temperature: 85.5 } }

Parameters:

  • id (Integer)

    The CAN message ID

  • data (Array<Integer>)

    The CAN message data bytes

Returns:

  • (Hash, nil)

    A hash containing :name (String) and :signals (Hash), or nil if no matching message



185
186
187
188
189
190
# File 'lib/can_messenger/dbc.rb', line 185

def decode_can(id, data)
  msg = @messages.values.find { |m| m.id == id }
  return nil unless msg

  { name: msg.name, signals: msg.decode(data) }
end

#encode_can(name, values) ⇒ Hash

Encodes signal values into a CAN message frame.

Takes a message name and a hash of signal values, then encodes them into the appropriate byte array according to the DBC signal definitions.

Examples:

frame = dbc.encode_can('EngineData', RPM: 2500, Temperature: 85.5)
# => { id: 0x123, data: [0x09, 0xC4, 0xAB, 0x00, 0x00, 0x00, 0x00, 0x00] }

Parameters:

  • name (String)

    The name of the message to encode

  • values (Hash<Symbol|String, Numeric>)

    Signal names mapped to their values

Returns:

  • (Hash)

    A hash containing :id (Integer) and :data (Array<Integer>)

Raises:

  • (ArgumentError)

    If the message name is not found in the DBC



166
167
168
169
170
171
# File 'lib/can_messenger/dbc.rb', line 166

def encode_can(name, values)
  msg = @messages[name]
  raise ArgumentError, "Unknown message #{name}" unless msg

  { id: msg.id, data: msg.encode(values) }
end

#parse(content) ⇒ void

This method returns an undefined value.

Parses DBC content and populates the messages hash.

This method processes each line of the DBC content, identifying message definitions (BO_) and signal definitions (SG_). It builds a complete message structure with all associated signals.

Parameters:

  • content (String)

    The DBC file content to parse



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/can_messenger/dbc.rb', line 66

def parse(content) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
  current = nil
  in_namespace_section = false

  # The parser is intentionally strict so malformed DBC files fail early with line context.
  # We still ignore a small allowlist of standard metadata directives that do not affect encode/decode.
  content.each_line.with_index(1) do |line, line_number|
    raw_line = line.rstrip
    stripped_line = raw_line.strip

    if in_namespace_section
      next if raw_line.match?(/^\s+/)

      in_namespace_section = false
    end

    next if stripped_line.empty?

    if ignored_line?(stripped_line)
      in_namespace_section = stripped_line.start_with?("NS_")
      next
    end

    if stripped_line.start_with?("BO_")
      msg = parse_message_line(stripped_line)
      raise_parse_error("Invalid message definition", line_number, stripped_line) unless msg

      current = msg
      @messages[msg.name] = msg
    elsif stripped_line.start_with?("SG_")
      raise_parse_error("Signal definition without a current message", line_number, stripped_line) unless current

      sig = parse_signal_line(stripped_line, current)
      raise_parse_error("Invalid signal definition", line_number, stripped_line) unless sig

      current.signals << sig
    else
      raise_parse_error("Unsupported or malformed DBC line", line_number, stripped_line)
    end
  end
end

#parse_message_line(line) ⇒ Message?

Parses a message definition line from DBC content.

Message lines follow the format: BO_ <ID> <Name>: <DLC> <Node>

Parameters:

  • line (String)

    A single line from the DBC file

Returns:

  • (Message, nil)

    A Message object if the line matches, nil otherwise



114
115
116
117
118
119
120
121
# File 'lib/can_messenger/dbc.rb', line 114

def parse_message_line(line)
  return unless (m = line.match(MESSAGE_LINE_PATTERN))

  id = m[1].to_i
  name = m[2]
  dlc = m[3].to_i
  Message.new(id, name, dlc)
end

#parse_signal_line(line, _current) ⇒ Signal?

Parses a signal definition line from DBC content.

Signal lines follow the format: SG_ <SignalName> : <StartBit>|<Length>@<Endianness><Sign> (<Factor>,<Offset>) [<Min>|<Max>] “<Unit>” <Receivers>

Parameters:

  • line (String)

    A single line from the DBC file

  • _current (Message)

    The current message being processed (unused but kept for API consistency)

Returns:

  • (Signal, nil)

    A Signal object if the line matches, nil otherwise



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/can_messenger/dbc.rb', line 131

def parse_signal_line(line, _current) # rubocop:disable Metrics/MethodLength
  return unless (m = line.match(SIGNAL_LINE_PATTERN))

  sig_name = m[1]
  start_bit = m[2].to_i
  length = m[3].to_i
  endian = m[4] == "1" ? :little : :big
  sign = m[5] == "-" ? :signed : :unsigned
  factor = m[6].to_f
  offset = m[7].to_f

  Signal.new(
    sig_name,
    start_bit: start_bit,
    length: length,
    endianness: endian,
    sign: sign,
    factor: factor,
    offset: offset
  )
end