can_messenger 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a68f4a339bebc034ffe78e990086ff91d28164962c2877e5d88edb095cbca90
4
- data.tar.gz: 7a96c22ccf82cae952123d733805c2efe31dd482ba9bf5d5a66d29cb14572822
3
+ metadata.gz: d6460da6eaff67f984db1e7f63466b8350140b244c4e7beaea56595a95e7da92
4
+ data.tar.gz: 7f06c6f54c0e6fc7ea49aff994ad7ad076c7d3a5f98cb50554793751f8941372
5
5
  SHA512:
6
- metadata.gz: c8302565cc5e889a290a446167bdec8eedd29ba077c63c003f544138c078a94498b258f465387b3c92f8b9b9ffc18ed65810fa70c5dc97e3ba977e62bbc394c4
7
- data.tar.gz: a606c71e8606bbccd98e1917bc4c2205037156e3994f152f6b05c3a52fc1037c21f8a38a91814abddb6f4a9bf5a0eb8682a2acaf93e3da0cdb818a2d8d03131f
6
+ metadata.gz: b1b3bdb759e1ed223c0e08d43c7fdcbf556123c007ecad5dce1b2b4b9b8ac35af289b64a15d16c46939a6ee9028d52b5015f35f4576b14637406884e9e881270
7
+ data.tar.gz: 1de19b416ecf4733f074c7c704a966313a86c6f16d9a631f7ff14a5b3ebf9b923dfe24930948bbb910554a376e168f49ea1a6c5c8abd324902efbdc9ed65366d
data/README.md CHANGED
@@ -6,6 +6,7 @@
6
6
  ![Status](https://img.shields.io/badge/status-stable-green)
7
7
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
8
8
  ![Gem Total Downloads](https://img.shields.io/gem/dt/can_messenger)
9
+ ![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/fk1018/can_messenger?utm_source=oss&utm_medium=github&utm_campaign=fk1018%2Fcan_messenger&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews)
9
10
 
10
11
  `can_messenger` is a Ruby gem that provides an interface for communicating over the CAN bus, allowing users to send and receive CAN messages `via raw SocketCAN sockets`. This gem is designed for developers who need an easy way to interact with CAN-enabled devices on Linux.
11
12
 
@@ -108,6 +109,24 @@ The `start_listening` method supports filtering incoming messages based on CAN I
108
109
  end
109
110
  ```
110
111
 
112
+ ### Working with DBC Files
113
+
114
+ Parse a DBC file and let the messenger encode and decode messages automatically:
115
+
116
+ ```ruby
117
+ dbc = CanMessenger::DBC.load('example.dbc')
118
+
119
+ # Encode using signal values
120
+ messenger.send_dbc_message(dbc: dbc, message_name: 'Example', signals: { Speed: 100 })
121
+
122
+ # Decode received frames
123
+ messenger.start_listening(dbc: dbc) do |msg|
124
+ if msg[:decoded]
125
+ puts "#{msg[:decoded][:name]} => #{msg[:decoded][:signals]}"
126
+ end
127
+ end
128
+ ```
129
+
111
130
  ### Stopping the Listener
112
131
 
113
132
  To stop listening, use:
@@ -168,6 +187,7 @@ Before using `can_messenger`, please note the following:
168
187
  - **Receive CAN Messages**: Continuously listen for messages on a CAN interface.
169
188
  - **Filtering**: Optional ID filters for incoming messages (single ID, range, or array).
170
189
  - **Logging**: Logs errors and events for debugging/troubleshooting.
190
+ - **DBC Parsing**: Parse DBC files to encode messages by name and decode incoming frames.
171
191
 
172
192
  ## Development
173
193
 
@@ -0,0 +1,516 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanMessenger
4
+ # DBC (Database CAN) Parser and Encoder/Decoder
5
+ #
6
+ # This class provides functionality to parse DBC files and encode/decode CAN messages
7
+ # according to the signal definitions. DBC files are a standard way to describe
8
+ # CAN network communication.
9
+ #
10
+ # @example Loading and using a DBC file
11
+ # dbc = CanMessenger::DBC.load('vehicle.dbc')
12
+ #
13
+ # # Encode a message with signal values
14
+ # frame = dbc.encode_can('EngineData', RPM: 2500, Temperature: 85.5)
15
+ # # => { id: 0x123, data: [0x09, 0xC4, 0xAB, 0x00, 0x00, 0x00, 0x00, 0x00] }
16
+ #
17
+ # # Decode a received CAN frame
18
+ # decoded = dbc.decode_can(0x123, [0x09, 0xC4, 0xAB, 0x00, 0x00, 0x00, 0x00, 0x00])
19
+ # # => { name: 'EngineData', signals: { RPM: 2500.0, Temperature: 85.5 } }
20
+ class DBC
21
+ attr_reader :messages
22
+
23
+ # Loads a DBC file from disk and parses its contents.
24
+ #
25
+ # @param [String] path The filesystem path to the DBC file
26
+ # @return [DBC] A new DBC instance with parsed message definitions
27
+ # @raise [Errno::ENOENT] If the file doesn't exist
28
+ # @raise [ArgumentError] If the file contains invalid DBC syntax
29
+ def self.load(path)
30
+ new(File.read(path))
31
+ end
32
+
33
+ # Initializes a new DBC instance.
34
+ #
35
+ # @param [String] content The DBC file content to parse (optional)
36
+ def initialize(content = "")
37
+ @messages = {}
38
+ parse(content) unless content.empty?
39
+ end
40
+
41
+ # Parses DBC content and populates the messages hash.
42
+ #
43
+ # This method processes each line of the DBC content, identifying message
44
+ # definitions (BO_) and signal definitions (SG_). It builds a complete
45
+ # message structure with all associated signals.
46
+ #
47
+ # @param [String] content The DBC file content to parse
48
+ # @return [void]
49
+ def parse(content) # rubocop:disable Metrics/MethodLength
50
+ current = nil
51
+ content.each_line do |line|
52
+ line.strip!
53
+ next if line.empty? || line.start_with?("BO_TX_BU_")
54
+
55
+ if (msg = parse_message_line(line))
56
+ current = msg
57
+ @messages[msg.name] = msg
58
+ elsif current && (sig = parse_signal_line(line, current))
59
+ current.signals << sig
60
+ end
61
+ end
62
+ end
63
+
64
+ # Parses a message definition line from DBC content.
65
+ #
66
+ # Message lines follow the format: BO_ <ID> <Name>: <DLC> <Node>
67
+ #
68
+ # @param [String] line A single line from the DBC file
69
+ # @return [Message, nil] A Message object if the line matches, nil otherwise
70
+ def parse_message_line(line)
71
+ return unless (m = line.match(/^BO_\s+(\d+)\s+(\w+)\s*:\s*(\d+)/))
72
+
73
+ id = m[1].to_i
74
+ name = m[2]
75
+ dlc = m[3].to_i
76
+ Message.new(id, name, dlc)
77
+ end
78
+
79
+ # Parses a signal definition line from DBC content.
80
+ #
81
+ # Signal lines follow the format:
82
+ # SG_ <SignalName> : <StartBit>|<Length>@<Endianness><Sign> (<Factor>,<Offset>) [<Min>|<Max>] "<Unit>" <Receivers>
83
+ #
84
+ # @param [String] line A single line from the DBC file
85
+ # @param [Message] _current The current message being processed (unused but kept for API consistency)
86
+ # @return [Signal, nil] A Signal object if the line matches, nil otherwise
87
+ def parse_signal_line(line, _current) # rubocop:disable Metrics/MethodLength
88
+ return unless (m = line.match(/^SG_\s+(\w+)\s*:\s*(\d+)\|(\d+)@(\d)([+-])\s*\(([^,]+),([^\)]+)\)/))
89
+
90
+ sig_name = m[1]
91
+ start_bit = m[2].to_i
92
+ length = m[3].to_i
93
+ endian = m[4] == "1" ? :little : :big
94
+ sign = m[5] == "-" ? :signed : :unsigned
95
+ factor = m[6].to_f
96
+ offset = m[7].to_f
97
+
98
+ Signal.new(
99
+ sig_name,
100
+ start_bit: start_bit,
101
+ length: length,
102
+ endianness: endian,
103
+ sign: sign,
104
+ factor: factor,
105
+ offset: offset
106
+ )
107
+ end # rubocop:enable Metrics/MethodLength
108
+
109
+ # Encodes signal values into a CAN message frame.
110
+ #
111
+ # Takes a message name and a hash of signal values, then encodes them
112
+ # into the appropriate byte array according to the DBC signal definitions.
113
+ #
114
+ # @param [String] name The name of the message to encode
115
+ # @param [Hash<Symbol|String, Numeric>] values Signal names mapped to their values
116
+ # @return [Hash] A hash containing :id (Integer) and :data (Array<Integer>)
117
+ # @raise [ArgumentError] If the message name is not found in the DBC
118
+ #
119
+ # @example
120
+ # frame = dbc.encode_can('EngineData', RPM: 2500, Temperature: 85.5)
121
+ # # => { id: 0x123, data: [0x09, 0xC4, 0xAB, 0x00, 0x00, 0x00, 0x00, 0x00] }
122
+ def encode_can(name, values)
123
+ msg = @messages[name]
124
+ raise ArgumentError, "Unknown message #{name}" unless msg
125
+
126
+ { id: msg.id, data: msg.encode(values) }
127
+ end
128
+
129
+ # Decodes a CAN message frame into signal values.
130
+ #
131
+ # Takes a CAN ID and data bytes, finds the matching message definition,
132
+ # and decodes the data into individual signal values according to the DBC.
133
+ #
134
+ # @param [Integer] id The CAN message ID
135
+ # @param [Array<Integer>] data The CAN message data bytes
136
+ # @return [Hash, nil] A hash containing :name (String) and :signals (Hash), or nil if no matching message
137
+ #
138
+ # @example
139
+ # decoded = dbc.decode_can(0x123, [0x09, 0xC4, 0xAB, 0x00, 0x00, 0x00, 0x00, 0x00])
140
+ # # => { name: 'EngineData', signals: { RPM: 2500.0, Temperature: 85.5 } }
141
+ def decode_can(id, data)
142
+ msg = @messages.values.find { |m| m.id == id }
143
+ return nil unless msg
144
+
145
+ { name: msg.name, signals: msg.decode(data) }
146
+ end
147
+ end
148
+
149
+ # Represents a CAN message definition from a DBC file.
150
+ #
151
+ # A Message contains the basic message properties (ID, name, data length)
152
+ # and a collection of Signal objects that define how data is structured
153
+ # within the message payload.
154
+ #
155
+ # @example
156
+ # message = Message.new(0x123, 'EngineData', 8)
157
+ # message.signals << Signal.new('RPM', start_bit: 0, length: 16, ...)
158
+ class Message
159
+ attr_reader :id, :name, :dlc, :signals
160
+
161
+ # Initializes a new Message instance.
162
+ #
163
+ # @param [Integer] id The CAN message ID (11-bit standard or 29-bit extended)
164
+ # @param [String] name The symbolic name of the message
165
+ # @param [Integer] dlc Data Length Code - number of bytes in the message (0-8 for classic CAN)
166
+ def initialize(id, name, dlc)
167
+ @id = id
168
+ @name = name
169
+ @dlc = dlc
170
+ @signals = []
171
+ end
172
+
173
+ # Encodes signal values into the message byte array.
174
+ #
175
+ # Iterates through all signals in the message and encodes their values
176
+ # into the appropriate bit positions within the message data bytes.
177
+ #
178
+ # @param [Hash<Symbol|String, Numeric>] values Signal names mapped to their values
179
+ # @return [Array<Integer>] Array of bytes representing the encoded message
180
+ def encode(values)
181
+ bytes = Array.new(@dlc, 0)
182
+ @signals.each do |sig|
183
+ next unless values.key?(sig.name.to_sym) || values.key?(sig.name.to_s)
184
+
185
+ v = values[sig.name.to_sym] || values[sig.name.to_s]
186
+ sig.encode(bytes, v)
187
+ end
188
+ bytes
189
+ end
190
+
191
+ # Decodes message data bytes into individual signal values.
192
+ #
193
+ # Extracts and decodes each signal from the message data bytes,
194
+ # applying the appropriate scaling (factor/offset) to produce
195
+ # the final engineering unit values.
196
+ #
197
+ # @param [Array<Integer>] data The message data bytes to decode
198
+ # @return [Hash<Symbol, Float>] Signal names mapped to their decoded values
199
+ def decode(data)
200
+ res = {}
201
+ @signals.each do |sig|
202
+ res[sig.name.to_sym] = sig.decode(data)
203
+ end
204
+ res
205
+ end
206
+ end
207
+
208
+ # Represents a signal within a CAN message.
209
+ #
210
+ # A Signal defines how a piece of data is encoded within a CAN message,
211
+ # including its bit position, length, byte order, signedness, and scaling.
212
+ # Signals can represent physical values (like temperature, speed) that are
213
+ # encoded as integers in the CAN frame but scaled to engineering units.
214
+ #
215
+ # @example Creating a signal for engine RPM
216
+ # rpm_signal = Signal.new('RPM',
217
+ # start_bit: 0, # Starting at bit 0
218
+ # length: 16, # 16 bits long
219
+ # endianness: :little, # Little-endian byte order
220
+ # sign: :unsigned, # Unsigned integer
221
+ # factor: 0.25, # Scale by 0.25
222
+ # offset: 0 # No offset
223
+ # )
224
+ class Signal # rubocop:disable Metrics/ClassLength
225
+ attr_reader :name, :start_bit, :length, :endianness, :sign, :factor, :offset
226
+
227
+ # Initializes a new Signal instance.
228
+ #
229
+ # @param [String] name The signal name
230
+ # @param [Integer] start_bit The starting bit position within the message (0-based)
231
+ # @param [Integer] length The number of bits the signal occupies (1-64)
232
+ # @param [Symbol] endianness Byte order - :little for little-endian, :big for big-endian
233
+ # @param [Symbol] sign Value type - :unsigned for unsigned integers, :signed for signed integers
234
+ # @param [Float] factor Scaling factor to convert raw value to engineering units
235
+ # @param [Float] offset Offset to add after scaling
236
+ def initialize(name, start_bit:, length:, endianness:, sign:, factor:, offset:) # rubocop:disable Metrics/ParameterLists
237
+ @name = name
238
+ @start_bit = start_bit
239
+ @length = length
240
+ @endianness = endianness
241
+ @sign = sign
242
+ @factor = factor
243
+ @offset = offset
244
+ end
245
+
246
+ # Encodes a value into the message byte array at this signal's bit position.
247
+ #
248
+ # Converts the engineering unit value to a raw integer using the signal's
249
+ # factor and offset, then places the bits into the appropriate positions
250
+ # within the message bytes.
251
+ #
252
+ # @param [Array<Integer>] bytes The message byte array to modify
253
+ # @param [Numeric] value The engineering unit value to encode
254
+ # @return [void]
255
+ # @raise [ArgumentError] If the value is out of range or signal exceeds message bounds
256
+ def encode(bytes, value)
257
+ raw = ((value - offset) / factor).round
258
+ validate_signal_bounds(bytes.size)
259
+ insert_bits(bytes, raw)
260
+ end
261
+
262
+ # Decodes this signal's value from the message byte array.
263
+ #
264
+ # Extracts the raw integer value from the appropriate bit positions,
265
+ # then applies the signal's scaling (factor and offset) to convert
266
+ # it to engineering units.
267
+ #
268
+ # @param [Array<Integer>] bytes The message byte array to decode from
269
+ # @return [Float] The decoded value in engineering units
270
+ def decode(bytes)
271
+ raw = extract_bits(bytes)
272
+ (raw * factor) + offset
273
+ end
274
+
275
+ private
276
+
277
+ # Validates that the signal fits within the message boundaries.
278
+ #
279
+ # Ensures that all bits used by this signal fall within the message's
280
+ # data length code (DLC) boundaries.
281
+ #
282
+ # @param [Integer] message_size_bytes The size of the message in bytes
283
+ # @return [void]
284
+ # @raise [ArgumentError] If signal bits exceed message boundaries or start_bit is negative
285
+ def validate_signal_bounds(message_size_bytes)
286
+ max_bit = start_bit + length - 1
287
+ max_allowed_bit = (message_size_bytes * 8) - 1
288
+
289
+ raise ArgumentError, "Signal #{name}: start_bit (#{start_bit}) cannot be negative" if start_bit.negative?
290
+
291
+ return unless max_bit > max_allowed_bit
292
+
293
+ raise ArgumentError,
294
+ "Signal #{name}: signal bits #{start_bit}..#{max_bit} exceed message size " \
295
+ "(#{message_size_bytes} bytes = #{max_allowed_bit + 1} bits)"
296
+ end
297
+
298
+ # Encodes a raw integer value into the message byte array.
299
+ #
300
+ # This is the main encoding method that coordinates validation,
301
+ # value processing, and bit manipulation.
302
+ #
303
+ # @param [Array<Integer>] bytes The message byte array to modify
304
+ # @param [Integer] raw The raw integer value to encode
305
+ # @return [void]
306
+ def insert_bits(bytes, raw)
307
+ validate_raw_value(raw)
308
+ processed_raw = process_raw_value(raw)
309
+ write_bits_to_bytes(bytes, processed_raw)
310
+ end
311
+
312
+ # Validates the raw integer value before encoding.
313
+ #
314
+ # Performs range checking for both signed and unsigned values
315
+ # to ensure they fit within the signal's bit length.
316
+ #
317
+ # @param [Integer] raw The raw value to validate
318
+ # @return [void]
319
+ # @raise [ArgumentError] If the value is out of range for the signal type
320
+ def validate_raw_value(raw)
321
+ validate_unsigned_value(raw)
322
+ validate_signed_value(raw)
323
+ end
324
+
325
+ # Validates unsigned values to ensure they're not negative.
326
+ #
327
+ # @param [Integer] raw The raw value to validate
328
+ # @return [void]
329
+ # @raise [ArgumentError] If an unsigned value is negative
330
+ def validate_unsigned_value(raw)
331
+ return unless sign == :unsigned && raw.negative?
332
+
333
+ raise ArgumentError, "Unsigned value cannot be negative: #{raw}"
334
+ end
335
+
336
+ # Validates signed values to ensure they fit in the signal's bit range.
337
+ #
338
+ # For signed signals, checks that the value fits within the two's complement
339
+ # range defined by the signal's bit length.
340
+ #
341
+ # @param [Integer] raw The raw value to validate
342
+ # @return [void]
343
+ # @raise [ArgumentError] If a signed value exceeds the bit field's range
344
+ def validate_signed_value(raw)
345
+ return unless sign == :signed
346
+
347
+ min_val = -(1 << (length - 1))
348
+ max_val = (1 << (length - 1)) - 1
349
+ return if raw.between?(min_val, max_val)
350
+
351
+ raise ArgumentError,
352
+ "Signed value #{raw} out of range [#{min_val}..#{max_val}] for #{length}-bit field"
353
+ end
354
+
355
+ # Processes the raw value for encoding (handles two's complement conversion).
356
+ #
357
+ # For signed negative values, converts them to two's complement representation.
358
+ # Ensures the final value fits within the signal's bit length.
359
+ #
360
+ # @param [Integer] raw The raw value to process
361
+ # @return [Integer] The processed value ready for bit manipulation
362
+ def process_raw_value(raw)
363
+ # Handle signed values: convert negative to two's complement
364
+ raw = (1 << length) + raw if sign == :signed && raw.negative?
365
+ # Ensure the value fits in the specified bit length
366
+ raw & ((1 << length) - 1)
367
+ end
368
+
369
+ # Writes the processed bits into the message byte array.
370
+ #
371
+ # Iterates through each bit of the signal and places it in the correct
372
+ # position within the message bytes, respecting the signal's endianness.
373
+ #
374
+ # @param [Array<Integer>] bytes The message byte array to modify
375
+ # @param [Integer] raw The processed value to write
376
+ # @return [void]
377
+ def write_bits_to_bytes(bytes, raw)
378
+ length.times do |i|
379
+ bit = (raw >> i) & 1
380
+ bit_pos = calculate_bit_position(i)
381
+ byte_index, bit_index = calculate_byte_and_bit_indices(bit_pos)
382
+
383
+ validate_bit_position(bit_pos, bytes.size)
384
+ update_byte_with_bit(bytes, byte_index, bit_index, bit)
385
+ end
386
+ end
387
+
388
+ # Calculates the bit position for a given bit offset within the signal.
389
+ #
390
+ # Handles both little-endian and big-endian bit ordering according
391
+ # to the signal's endianness setting.
392
+ #
393
+ # @param [Integer] bit_offset The offset within the signal (0 to length-1)
394
+ # @return [Integer] The absolute bit position within the message
395
+ def calculate_bit_position(bit_offset)
396
+ if endianness == :little
397
+ start_bit + bit_offset
398
+ else
399
+ # For big-endian signals, the bit numbering within a byte follows MSB-first
400
+ # ordering. This means that the most significant bit (MSB) is numbered 7,
401
+ # and the least significant bit (LSB) is numbered 0. To calculate the absolute
402
+ # bit position, we first determine the position of the MSB in the starting byte.
403
+ #
404
+ # The formula ((start_bit / 8) * 8) calculates the starting byte's base bit
405
+ # position (aligned to the nearest multiple of 8). Adding (7 - (start_bit % 8))
406
+ # adjusts this base position to point to the MSB of the starting byte.
407
+ #
408
+ # Finally, we subtract the bit offset to account for the signal's length and
409
+ # position within the message.
410
+ base = ((start_bit / 8) * 8) + (7 - (start_bit % 8))
411
+ base - bit_offset
412
+ end
413
+ end
414
+
415
+ # Calculates byte and bit indices from an absolute bit position.
416
+ #
417
+ # @param [Integer] bit_pos The absolute bit position within the message
418
+ # @return [Array<Integer>] A two-element array [byte_index, bit_index]
419
+ def calculate_byte_and_bit_indices(bit_pos)
420
+ [bit_pos / 8, bit_pos % 8]
421
+ end
422
+
423
+ # Validates that a bit position is within the message boundaries.
424
+ #
425
+ # @param [Integer] bit_pos The bit position to validate
426
+ # @param [Integer] bytes_size The size of the message in bytes
427
+ # @return [void]
428
+ # @raise [ArgumentError] If the bit position is out of bounds
429
+ def validate_bit_position(bit_pos, bytes_size)
430
+ byte_index = bit_pos / 8
431
+ return unless byte_index >= bytes_size || byte_index.negative?
432
+
433
+ raise ArgumentError, "Bit position #{bit_pos} out of bounds"
434
+ end
435
+
436
+ # Updates a specific bit within a byte in the message array.
437
+ #
438
+ # Sets or clears the specified bit within the target byte, initializing
439
+ # the byte to 0 if it hasn't been set yet.
440
+ #
441
+ # @param [Array<Integer>] bytes The message byte array to modify
442
+ # @param [Integer] byte_index The index of the byte to modify
443
+ # @param [Integer] bit_index The bit position within the byte (0-7)
444
+ # @param [Integer] bit The bit value to set (0 or 1)
445
+ # @return [void]
446
+ def update_byte_with_bit(bytes, byte_index, bit_index, bit)
447
+ bytes[byte_index] ||= 0
448
+ if bit == 1
449
+ bytes[byte_index] |= (1 << bit_index)
450
+ else
451
+ bytes[byte_index] &= ~(1 << bit_index)
452
+ end
453
+ end
454
+
455
+ # Extracts the signal value from the message byte array.
456
+ #
457
+ # This is the main decoding method that coordinates bit extraction
458
+ # and sign conversion.
459
+ #
460
+ # @param [Array<Integer>] bytes The message byte array to decode from
461
+ # @return [Integer] The raw integer value extracted from the message
462
+ def extract_bits(bytes)
463
+ value = read_bits_from_bytes(bytes)
464
+ convert_to_signed_if_needed(value)
465
+ end
466
+
467
+ # Reads the raw bits from the message byte array.
468
+ #
469
+ # Extracts each bit of the signal from the message bytes, building
470
+ # up the raw integer value bit by bit.
471
+ #
472
+ # @param [Array<Integer>] bytes The message byte array to read from
473
+ # @return [Integer] The raw unsigned integer value
474
+ def read_bits_from_bytes(bytes)
475
+ value = 0
476
+ length.times do |i|
477
+ bit_pos = calculate_bit_position(i)
478
+ byte_index, bit_index = calculate_byte_and_bit_indices(bit_pos)
479
+
480
+ validate_extraction_bounds(bit_pos, bytes.size)
481
+
482
+ bit = ((bytes[byte_index] || 0) >> bit_index) & 1
483
+ value |= (bit << i)
484
+ end
485
+ value
486
+ end
487
+
488
+ # Validates bit position during extraction to ensure it's within bounds.
489
+ #
490
+ # @param [Integer] bit_pos The bit position to validate
491
+ # @param [Integer] bytes_size The size of the message in bytes
492
+ # @return [void]
493
+ # @raise [ArgumentError] If the bit position is out of bounds
494
+ def validate_extraction_bounds(bit_pos, bytes_size)
495
+ byte_index = bit_pos / 8
496
+ return unless byte_index >= bytes_size || byte_index.negative?
497
+
498
+ raise ArgumentError, "Bit position #{bit_pos} out of bounds during extraction"
499
+ end
500
+
501
+ # Converts unsigned value to signed if the signal is signed and the MSB is set.
502
+ #
503
+ # For signed signals, checks if the most significant bit is set and
504
+ # converts the value from two's complement representation to a negative integer.
505
+ #
506
+ # @param [Integer] value The unsigned integer value to potentially convert
507
+ # @return [Integer] The final signed or unsigned value
508
+ def convert_to_signed_if_needed(value)
509
+ if sign == :signed && value[length - 1] == 1
510
+ value - (1 << length)
511
+ else
512
+ value
513
+ end
514
+ end
515
+ end
516
+ end
@@ -42,6 +42,8 @@ module CanMessenger
42
42
  # @param [Array<Integer>] data The data bytes of the CAN message (0 to 8 bytes).
43
43
  # @return [void]
44
44
  def send_can_message(id:, data:, extended_id: false, can_fd: nil)
45
+ raise ArgumentError, "id and data are required" if id.nil? || data.nil?
46
+
45
47
  use_fd = can_fd.nil? ? @can_fd : can_fd
46
48
 
47
49
  with_socket(can_fd: use_fd) do |socket|
@@ -54,6 +56,23 @@ module CanMessenger
54
56
  @logger.error("Error sending CAN message (ID: #{id}): #{e}")
55
57
  end
56
58
 
59
+ # Encodes and sends a CAN message using a DBC definition
60
+ #
61
+ # @param [String] message_name The message name to encode
62
+ # @param [Hash] signals Values for each signal in the message
63
+ # @param [CanMessenger::DBC] dbc The DBC instance used for encoding (defaults to @dbc)
64
+ # @return [void]
65
+ def send_dbc_message(message_name:, signals:, dbc: @dbc, extended_id: false, can_fd: nil)
66
+ raise ArgumentError, "dbc is required" if dbc.nil?
67
+
68
+ encoded = dbc.encode_can(message_name, signals)
69
+ send_can_message(id: encoded[:id], data: encoded[:data], extended_id: extended_id, can_fd: can_fd)
70
+ rescue ArgumentError
71
+ raise
72
+ rescue StandardError => e
73
+ @logger.error("Error sending DBC message #{message_name}: #{e}")
74
+ end
75
+
57
76
  # Continuously listens for CAN messages on the specified interface.
58
77
  #
59
78
  # This method listens for incoming CAN messages and applies an optional filter.
@@ -67,7 +86,7 @@ module CanMessenger
67
86
  # - `:id` [Integer] the CAN message ID
68
87
  # - `:data` [Array<Integer>] the message data bytes
69
88
  # @return [void]
70
- def start_listening(filter: nil, can_fd: nil, &block)
89
+ def start_listening(filter: nil, can_fd: nil, dbc: nil, &block)
71
90
  return @logger.error("No block provided to handle messages.") unless block_given?
72
91
 
73
92
  @listening = true
@@ -76,7 +95,7 @@ module CanMessenger
76
95
 
77
96
  with_socket(can_fd: use_fd) do |socket|
78
97
  @logger.info("Started listening on #{@interface_name}")
79
- process_message(socket, filter, use_fd, &block) while @listening
98
+ process_message(socket, filter, use_fd, dbc, &block) while @listening
80
99
  end
81
100
  end
82
101
 
@@ -161,12 +180,17 @@ module CanMessenger
161
180
  # @param filter [Integer, Range, Array<Integer>, nil] Optional filter for CAN IDs.
162
181
  # @yield [message] Yields the message if it passes filtering.
163
182
  # @return [void]
164
- def process_message(socket, filter, can_fd)
183
+ def process_message(socket, filter, can_fd, dbc, &block)
165
184
  message = receive_message(socket: socket, can_fd: can_fd)
166
185
  return if message.nil?
167
186
  return if filter && !matches_filter?(message_id: message[:id], filter: filter)
168
187
 
169
- yield(message)
188
+ if dbc
189
+ decoded = dbc.decode_can(message[:id], message[:data])
190
+ message[:decoded] = decoded if decoded
191
+ end
192
+
193
+ block.call(message)
170
194
  rescue StandardError => e
171
195
  @logger.error("Unexpected error in listening loop: #{e.message}")
172
196
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CanMessenger
4
- VERSION = "1.3.0"
4
+ VERSION = "1.4.0"
5
5
  end
data/lib/can_messenger.rb CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  require_relative "can_messenger/version"
5
5
  require_relative "can_messenger/messenger"
6
+ require_relative "can_messenger/dbc"
6
7
 
7
8
  module CanMessenger
8
9
  class Error < StandardError; end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: can_messenger
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - fk1018
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-07-08 00:00:00.000000000 Z
11
+ date: 2025-07-29 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: CanMessenger provides an interface to send and receive messages over
14
14
  the CAN bus, useful for applications requiring CAN communication in Ruby.
@@ -20,6 +20,7 @@ extra_rdoc_files: []
20
20
  files:
21
21
  - README.md
22
22
  - lib/can_messenger.rb
23
+ - lib/can_messenger/dbc.rb
23
24
  - lib/can_messenger/messenger.rb
24
25
  - lib/can_messenger/version.rb
25
26
  homepage: https://github.com/fk1018/can_messenger