can_messenger 1.2.1 → 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: d47c9c4b19c40337cb6b58f380e9a3f07554144a14d23ef4c2589060fd25942e
4
- data.tar.gz: 66f58f8fab19fb73ab5124e4036313459ae032282bdd07613dc6156ef86a1379
3
+ metadata.gz: d6460da6eaff67f984db1e7f63466b8350140b244c4e7beaea56595a95e7da92
4
+ data.tar.gz: 7f06c6f54c0e6fc7ea49aff994ad7ad076c7d3a5f98cb50554793751f8941372
5
5
  SHA512:
6
- metadata.gz: a55c719faf6f813ff4aef9004b4b32065297773498bf92291019002520b43c940e304fa5a0244635f42c9152ab3729cbeaa8534d809b2790878ebc9ef9a00244
7
- data.tar.gz: '014168fa0419b78aa966c4d1d9a415c2e8e70650323df2c1a4328354099ffacf038cd52ef1c1fc371334e312a91538e985b7ec98727863739aa70c88e449d4d3'
6
+ metadata.gz: b1b3bdb759e1ed223c0e08d43c7fdcbf556123c007ecad5dce1b2b4b9b8ac35af289b64a15d16c46939a6ee9028d52b5015f35f4576b14637406884e9e881270
7
+ data.tar.gz: 1de19b416ecf4733f074c7c704a966313a86c6f16d9a631f7ff14a5b3ebf9b923dfe24930948bbb910554a376e168f49ea1a6c5c8abd324902efbdc9ed65366d
data/README.md CHANGED
@@ -6,9 +6,14 @@
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
 
13
+ ## Requirements
14
+
15
+ - Ruby 3.0 or higher.
16
+
12
17
  ## Installation
13
18
 
14
19
  To install `can_messenger`, add it to your application's Gemfile:
@@ -57,6 +62,15 @@ If you need to send an extended CAN frame (29-bit ID), set extended_id: true. Th
57
62
  messenger.send_can_message(id: 0x123456, data: [0x01, 0x02, 0x03], extended_id: true)
58
63
  ```
59
64
 
65
+ If you need to work with **CAN FD** frames (up to 64 data bytes), enable the mode per call or when initializing the messenger:
66
+
67
+ ```ruby
68
+ messenger_fd = CanMessenger::Messenger.new(interface_name: 'can0', can_fd: true)
69
+ messenger_fd.send_can_message(id: 0x123, data: Array.new(12, 0xFF))
70
+ # Or on demand
71
+ messenger.send_can_message(id: 0x123, data: Array.new(12, 0xFF), can_fd: true)
72
+ ```
73
+
60
74
  ### Receiving CAN Messages
61
75
 
62
76
  To listen for incoming messages, set up a listener:
@@ -95,6 +109,24 @@ The `start_listening` method supports filtering incoming messages based on CAN I
95
109
  end
96
110
  ```
97
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
+
98
130
  ### Stopping the Listener
99
131
 
100
132
  To stop listening, use:
@@ -147,14 +179,15 @@ Before using `can_messenger`, please note the following:
147
179
 
148
180
  - **CAN Frame Format Assumptions:**
149
181
  - By default, the gem uses **big-endian** packing for CAN IDs. If you integrate with a system using little-endian, you may need to adjust or specify an endianness in the code.
150
- - The gem expects a standard CAN frame layout (16 bytes total, with the first 4 for the ID, followed by 1 byte for DLC, 3 bytes of padding, and up to 8 bytes of data). If you work with non-standard frames or CAN FD (64-byte data), you’ll need to customize the parsing/sending logic.
182
+ - The gem expects a standard CAN frame layout (16 bytes total, with the first 4 for the ID, followed by 1 byte for DLC, 3 bytes of padding, and up to 8 bytes of data). **CAN FD** frames (up to 64 bytes) are supported when enabled.
151
183
 
152
184
  ## Features
153
185
 
154
- - **Send CAN Messages**: Send CAN messages (up to 8 data bytes).
186
+ - **Send CAN Messages**: Send CAN messages (up to 8 data bytes, or 64 bytes with CAN FD enabled).
155
187
  - **Receive CAN Messages**: Continuously listen for messages on a CAN interface.
156
188
  - **Filtering**: Optional ID filters for incoming messages (single ID, range, or array).
157
189
  - **Logging**: Logs errors and events for debugging/troubleshooting.
190
+ - **DBC Parsing**: Parse DBC files to encode messages by name and decode incoming frames.
158
191
 
159
192
  ## Development
160
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
@@ -17,7 +17,9 @@ module CanMessenger
17
17
  # end
18
18
  class Messenger # rubocop:disable Metrics/ClassLength
19
19
  FRAME_SIZE = 16
20
+ CANFD_FRAME_SIZE = 72
20
21
  MIN_FRAME_SIZE = 8
22
+ MAX_FD_DATA = 64
21
23
  TIMEOUT = [1, 0].pack("l_2")
22
24
 
23
25
  # Initializes a new Messenger instance.
@@ -26,11 +28,12 @@ module CanMessenger
26
28
  # @param [Logger, nil] logger Optional logger for error handling and debug information.
27
29
  # @param [Symbol] endianness The endianness of the CAN ID (default: :big) can be :big or :little.
28
30
  # @return [void]
29
- def initialize(interface_name:, logger: nil, endianness: :big)
31
+ def initialize(interface_name:, logger: nil, endianness: :big, can_fd: false)
30
32
  @interface_name = interface_name
31
33
  @logger = logger || Logger.new($stdout)
32
34
  @listening = true # Control flag for listening loop
33
35
  @endianness = endianness # :big or :little
36
+ @can_fd = can_fd
34
37
  end
35
38
 
36
39
  # Sends a CAN message by writing directly to a raw CAN socket
@@ -38,9 +41,13 @@ module CanMessenger
38
41
  # @param [Integer] id The CAN ID of the message (up to 29 bits for extended IDs).
39
42
  # @param [Array<Integer>] data The data bytes of the CAN message (0 to 8 bytes).
40
43
  # @return [void]
41
- def send_can_message(id:, data:, extended_id: false)
42
- with_socket do |socket|
43
- frame = build_can_frame(id: id, data: data, extended_id: extended_id)
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
+
47
+ use_fd = can_fd.nil? ? @can_fd : can_fd
48
+
49
+ with_socket(can_fd: use_fd) do |socket|
50
+ frame = build_can_frame(id: id, data: data, extended_id: extended_id, can_fd: use_fd)
44
51
  socket.write(frame)
45
52
  end
46
53
  rescue ArgumentError
@@ -49,6 +56,23 @@ module CanMessenger
49
56
  @logger.error("Error sending CAN message (ID: #{id}): #{e}")
50
57
  end
51
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
+
52
76
  # Continuously listens for CAN messages on the specified interface.
53
77
  #
54
78
  # This method listens for incoming CAN messages and applies an optional filter.
@@ -62,14 +86,16 @@ module CanMessenger
62
86
  # - `:id` [Integer] the CAN message ID
63
87
  # - `:data` [Array<Integer>] the message data bytes
64
88
  # @return [void]
65
- def start_listening(filter: nil, &block)
89
+ def start_listening(filter: nil, can_fd: nil, dbc: nil, &block)
66
90
  return @logger.error("No block provided to handle messages.") unless block_given?
67
91
 
68
92
  @listening = true
69
93
 
70
- with_socket do |socket|
94
+ use_fd = can_fd.nil? ? @can_fd : can_fd
95
+
96
+ with_socket(can_fd: use_fd) do |socket|
71
97
  @logger.info("Started listening on #{@interface_name}")
72
- process_message(socket, filter, &block) while @listening
98
+ process_message(socket, filter, use_fd, dbc, &block) while @listening
73
99
  end
74
100
  end
75
101
 
@@ -88,8 +114,8 @@ module CanMessenger
88
114
  #
89
115
  # @yield [socket] An open CAN socket.
90
116
  # @return [void]
91
- def with_socket
92
- socket = open_can_socket
117
+ def with_socket(can_fd: @can_fd)
118
+ socket = open_can_socket(can_fd: can_fd)
93
119
  return @logger.error("Failed to open socket, cannot continue operation.") if socket.nil?
94
120
 
95
121
  yield socket
@@ -100,23 +126,32 @@ module CanMessenger
100
126
  # Creates and configures a CAN socket bound to @interface_name.
101
127
  #
102
128
  # @return [Socket, nil] The configured CAN socket, or nil if the socket cannot be opened.
103
- def open_can_socket
129
+ def open_can_socket(can_fd: @can_fd) # rubocop:disable Metrics/MethodLength
104
130
  socket = Socket.open(Socket::PF_CAN, Socket::SOCK_RAW, Socket::CAN_RAW)
105
131
  socket.bind(Socket.pack_sockaddr_can(@interface_name))
106
132
  socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, TIMEOUT)
133
+ if can_fd && Socket.const_defined?(:CAN_RAW_FD_FRAMES)
134
+ socket.setsockopt(Socket.const_defined?(:SOL_CAN_RAW) ? Socket::SOL_CAN_RAW : Socket::CAN_RAW,
135
+ Socket::CAN_RAW_FD_FRAMES, 1)
136
+ end
107
137
  socket
108
138
  rescue StandardError => e
109
139
  @logger.error("Error creating CAN socket on interface #{@interface_name}: #{e}")
110
140
  nil
111
141
  end
112
142
 
113
- # Builds a raw CAN frame for SocketCAN, big-endian ID, 1-byte DLC, up to 8 data bytes, and 3 padding bytes.
143
+ # Builds a raw CAN or CAN FD frame for SocketCAN.
114
144
  #
115
145
  # @param id [Integer] the CAN ID
116
- # @param data [Array<Integer>] up to 8 bytes
117
- # @return [String] a 16-byte string representing a classic CAN frame
118
- def build_can_frame(id:, data:, extended_id: false)
119
- raise ArgumentError, "CAN data cannot exceed 8 bytes" if data.size > 8
146
+ # @param data [Array<Integer>] data bytes (up to 8 for classic, 64 for CAN FD)
147
+ # @param can_fd [Boolean] whether to build a CAN FD frame
148
+ # @return [String] the packed CAN frame
149
+ def build_can_frame(id:, data:, extended_id: false, can_fd: false) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
150
+ if can_fd
151
+ raise ArgumentError, "CAN FD data cannot exceed #{MAX_FD_DATA} bytes" if data.size > MAX_FD_DATA
152
+ elsif data.size > 8
153
+ raise ArgumentError, "CAN data cannot exceed 8 bytes"
154
+ end
120
155
 
121
156
  # Mask the ID to 29 bits
122
157
  can_id = id & 0x1FFFFFFF
@@ -127,13 +162,15 @@ module CanMessenger
127
162
  # Pack the 4‐byte ID (big-endian or little-endian)
128
163
  id_bytes = @endianness == :big ? [can_id].pack("L>") : [can_id].pack("V")
129
164
 
130
- # 1 byte for DLC, then 3 bytes of padding
165
+ # 1 byte for DLC/length, then 3 bytes for flags/reserved
131
166
  dlc_and_pad = [data.size, 0, 0, 0].pack("C*")
132
167
 
133
- # Up to 8 data bytes, pad with 0 if fewer
134
- payload = data.pack("C*").ljust(8, "\x00")
168
+ payload = if can_fd
169
+ data.pack("C*").ljust(MAX_FD_DATA, "\x00")
170
+ else
171
+ data.pack("C*").ljust(8, "\x00")
172
+ end
135
173
 
136
- # Total 16 bytes (4 for ID, 1 for DLC, 3 padding, 8 data)
137
174
  id_bytes + dlc_and_pad + payload
138
175
  end
139
176
 
@@ -143,12 +180,17 @@ module CanMessenger
143
180
  # @param filter [Integer, Range, Array<Integer>, nil] Optional filter for CAN IDs.
144
181
  # @yield [message] Yields the message if it passes filtering.
145
182
  # @return [void]
146
- def process_message(socket, filter)
147
- message = receive_message(socket: socket)
183
+ def process_message(socket, filter, can_fd, dbc, &block)
184
+ message = receive_message(socket: socket, can_fd: can_fd)
148
185
  return if message.nil?
149
186
  return if filter && !matches_filter?(message_id: message[:id], filter: filter)
150
187
 
151
- 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)
152
194
  rescue StandardError => e
153
195
  @logger.error("Unexpected error in listening loop: #{e.message}")
154
196
  end
@@ -157,11 +199,12 @@ module CanMessenger
157
199
  #
158
200
  # @param socket [Socket]
159
201
  # @return [Hash, nil]
160
- def receive_message(socket:)
161
- frame = socket.recv(FRAME_SIZE)
202
+ def receive_message(socket:, can_fd: false)
203
+ frame_size = can_fd ? CANFD_FRAME_SIZE : FRAME_SIZE
204
+ frame = socket.recv(frame_size)
162
205
  return nil if frame.nil? || frame.size < MIN_FRAME_SIZE
163
206
 
164
- parse_frame(frame: frame)
207
+ parse_frame(frame: frame, can_fd: can_fd)
165
208
  rescue IO::WaitReadable
166
209
  nil
167
210
  rescue StandardError => e
@@ -173,9 +216,11 @@ module CanMessenger
173
216
  #
174
217
  # @param [String] frame
175
218
  # @return [Hash, nil]
176
- def parse_frame(frame:) # rubocop:disable Metrics/MethodLength
219
+ def parse_frame(frame:, can_fd: nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
177
220
  return nil unless frame && frame.size >= MIN_FRAME_SIZE
178
221
 
222
+ use_fd = can_fd.nil? ? frame.size >= CANFD_FRAME_SIZE : can_fd
223
+
179
224
  raw_id = unpack_frame_id(frame: frame)
180
225
 
181
226
  # Determine if EFF bit is set
@@ -185,8 +230,11 @@ module CanMessenger
185
230
  # Now mask off everything except the lower 29 bits
186
231
  id = raw_id & 0x1FFFFFFF
187
232
 
188
- # DLC is the lower 4 bits of byte 4
189
- data_length = frame[4].ord & 0x0F
233
+ data_length = if use_fd
234
+ frame[4].ord
235
+ else
236
+ frame[4].ord & 0x0F
237
+ end
190
238
 
191
239
  # Extract data
192
240
  data = if frame.size >= MIN_FRAME_SIZE + data_length
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CanMessenger
4
- VERSION = "1.2.1"
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.2.1
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-06-05 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