can_messenger 1.3.0 → 2.0.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: 15f09405e67a906115972dc84790769875d1dcdb5cbd78a57827ccdf910d7e05
4
+ data.tar.gz: faa23fc30c89b5a81875ff2664e5f97cac2970cb6161599887a1c02da9b5f36c
5
5
  SHA512:
6
- metadata.gz: c8302565cc5e889a290a446167bdec8eedd29ba077c63c003f544138c078a94498b258f465387b3c92f8b9b9ffc18ed65810fa70c5dc97e3ba977e62bbc394c4
7
- data.tar.gz: a606c71e8606bbccd98e1917bc4c2205037156e3994f152f6b05c3a52fc1037c21f8a38a91814abddb6f4a9bf5a0eb8682a2acaf93e3da0cdb818a2d8d03131f
6
+ metadata.gz: bb33fdda392ca2acaea5357a661b5b9dda245c41db5b000802e4d12886d58b16c49d5238312aa254f062fdc9f46c9f05b9f140f202a0ad73e1ceda9b81d397ca
7
+ data.tar.gz: 4823263138efc7d61756d473e2e6235487921f7d8e6d8350260dcd80d491bee6927cbf5606671a242576fdb3f3fb5437212f77c008856c3bfa1d2b9a5b5d4924
data/CHANGELOG.md ADDED
@@ -0,0 +1,165 @@
1
+ ## [Unreleased]
2
+
3
+ ## [2.0.0] - 2026-02-02
4
+
5
+ ### Changed
6
+
7
+ - **Breaking:** Require Ruby 4.0.1 or higher.
8
+ - Update CI to run on Ruby 4.0.1.
9
+ - Upgrade RuboCop to support Ruby 4.0 and refresh linting.
10
+ - Minor style cleanups in DBC parsing and message listener block forwarding.
11
+ - Extract SocketCAN logic into a dedicated adapter and add a base adapter interface.
12
+ - Allow injecting a custom adapter into `Messenger` for alternate transports or testing.
13
+ - **Breaking:** Default CAN ID endianness is now native (`:native`) instead of `:big`.
14
+
15
+ ### Fixed
16
+
17
+ - Close SocketCAN sockets when bind/setsockopt fails to avoid leaks.
18
+
19
+ ## [1.4.0] - 2025-07-25
20
+
21
+ ### Added
22
+
23
+ - `send_dbc_message` helper for encoding and sending messages defined in DBC files.
24
+
25
+ ### Changed
26
+
27
+ - `send_can_message` now only accepts raw frame parameters.
28
+ - DBC parsing code split into helper methods for clarity.
29
+
30
+ ### Fixed
31
+
32
+ - Correct encoding of negative signal values using two's-complement.
33
+
34
+ ## [1.3.0] - 2025-06-27
35
+
36
+ ### Added
37
+
38
+ - Optional **CAN FD** support for sending and receiving up to 64-byte frames.
39
+
40
+ ### Changed
41
+
42
+ - Updated APIs to accept a `can_fd:` flag on initialization and message methods.
43
+
44
+ ### Fixed
45
+
46
+ - (Nothing since last release.)
47
+
48
+ ## [1.2.1] - 2025-06-05
49
+
50
+ ### Changed
51
+
52
+ - `send_can_message` now raises `ArgumentError` when data length exceeds eight bytes.
53
+ - Updated RBS `initialize` signature to include the `endianness` argument.
54
+ - Fixed formatting in README around extended CAN frames.
55
+ - Clarified spec helper comment.
56
+
57
+ ### Fixed
58
+
59
+ - Addressed a listener restart bug allowing `start_listening` to be called again.
60
+
61
+ ## [1.2.0] - 2025-02-28
62
+
63
+ ### Added
64
+
65
+ - **Explicit extended CAN ID support**.
66
+ - Added an `extended_id: false` parameter to `send_can_message`, which, if set to `true`, sets the Extended Frame Format bit (bit 31) in the CAN ID.
67
+ - Updated `parse_frame` to detect and report `extended: true` when the EFF bit is set in incoming frames.
68
+ - Added corresponding tests for sending and receiving extended CAN frames.
69
+
70
+ ### Changed
71
+
72
+ - _No breaking changes_, but internal refactoring around how CAN IDs are packed and unpacked.
73
+ - Removed the masking of bit 31 in `unpack_frame_id`, ensuring extended frames are no longer silently treated as standard frames.
74
+
75
+ ### Fixed
76
+
77
+ - (Nothing since last release.)
78
+
79
+ ## [1.1.0] - 2025-02-10
80
+
81
+ ### Changed
82
+
83
+ - **Removed dependency on `cansend`**. We now write CAN frames directly via raw sockets.
84
+ - Internal refactoring to support raw-socket–based sending without changing the public API.
85
+
86
+ ### Fixed
87
+
88
+ ## [1.0.3] - 2025-02-09
89
+
90
+ - Revert release.yml
91
+
92
+ ### Fixed
93
+
94
+ ## [1.0.2] - 2025-02-09
95
+
96
+ - Bugfix release.yml
97
+
98
+ ## [1.0.1] - 2025-02-09
99
+
100
+ ### Changed
101
+
102
+ - Updated the README to include an **Important Considerations** section that outlines environment requirements, API changes (keyword arguments and block requirement), threading and socket management notes, and logging behavior.
103
+ - Made minor documentation clarifications and tweaks to help users avoid common pitfalls.
104
+
105
+ ## [1.0.0] - 2025-02-09
106
+
107
+ ### Changed
108
+
109
+ - **Breaking Change:** Updated the Messenger API to require keyword arguments (e.g., `interface_name:`) for initialization. Existing code using the old API will need to be updated.
110
+ - **Breaking Change:** Updated the send_can_message Method to require keyword arguments (e.g., `id:,data:`) to send messages to the can bus. Existing code using the old API will need to be updated.
111
+ - Refactored `start_listening`.
112
+ - Enhanced error handling throughout the gem with more detailed logging.
113
+ - Updated type signatures (RBS) and documentation to match the new API.
114
+ - Refactored tests to reflect the new API and improved error handling.
115
+
116
+ ## [0.2.3] - 2025-02-01
117
+
118
+ ### Changed
119
+
120
+ - Updated the internal listening loop in `start_listening` to continue iterating on nil (timeout) responses instead of breaking out, improving reliability.
121
+ - Suppressed log output during tests by injecting a silent logger.
122
+ - Updated the test suite to better handle long-running listening loops and error conditions.
123
+
124
+ ## [0.2.2] - 2024-12-06
125
+
126
+ ### Changed
127
+
128
+ - Updated README.md to reflect modern debian package install command.
129
+
130
+ ## [0.2.1] - 2024-12-06
131
+
132
+ ### Changed
133
+
134
+ - Updated `start_listening` RBS signature to include the `filter` parameter, ensuring type definitions match the implementation.
135
+
136
+ ## [0.2.0] - 2024-12-05
137
+
138
+ ### Added
139
+
140
+ - Filtering support for `start_listening` via a `filter` parameter:
141
+ - Single CAN ID.
142
+ - Range of CAN IDs.
143
+ - Array of CAN IDs.
144
+
145
+ ### Changed
146
+
147
+ - Refactored `start_listening` to support optional filtering of incoming CAN messages.
148
+ - Documentation updates for `start_listening` in README.
149
+
150
+ ## [0.1.0] - 2024-11-10
151
+
152
+ - Initial release
153
+ [2.0.0]: https://github.com/fk1018/can_messenger/compare/v1.3.0...v2.0.0
154
+ [1.3.0]: https://github.com/fk1018/can_messenger/compare/v1.2.1...v1.3.0
155
+ [1.2.1]: https://github.com/fk1018/can_messenger/compare/v1.2.0...v1.2.1
156
+ [1.2.0]: https://github.com/fk1018/can_messenger/compare/v1.1.0...v1.2.0
157
+ [1.1.0]: https://github.com/fk1018/can_messenger/compare/v1.0.3...v1.1.0
158
+ [1.0.3]: https://github.com/fk1018/can_messenger/compare/v1.0.1...v1.0.3
159
+ [1.0.1]: https://github.com/fk1018/can_messenger/compare/v1.0.0...v1.0.1
160
+ [1.0.0]: https://github.com/fk1018/can_messenger/compare/v0.2.3...v1.0.0
161
+ [0.2.3]: https://github.com/fk1018/can_messenger/compare/v0.2.2...v0.2.3
162
+ [0.2.2]: https://github.com/fk1018/can_messenger/compare/v0.2.1...v0.2.2
163
+ [0.2.1]: https://github.com/fk1018/can_messenger/compare/v0.2.0...v0.2.1
164
+ [0.2.0]: https://github.com/fk1018/can_messenger/compare/v0.1.0...v0.2.0
165
+ [0.1.0]: https://github.com/fk1018/can_messenger/releases/tag/v0.1.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 fk1018
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md CHANGED
@@ -6,12 +6,13 @@
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
 
12
13
  ## Requirements
13
14
 
14
- - Ruby 3.0 or higher.
15
+ - Ruby 4.0.1 or higher.
15
16
 
16
17
  ## Installation
17
18
 
@@ -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:
@@ -116,17 +135,34 @@ To stop listening, use:
116
135
  messenger.stop_listening
117
136
  ```
118
137
 
138
+ ### Adapters
139
+
140
+ `CanMessenger::Messenger` delegates low-level CAN bus operations to an adapter. By default it uses the
141
+ SocketCAN adapter which communicates with Linux CAN interfaces using raw sockets:
142
+
143
+ ```ruby
144
+ messenger = CanMessenger::Messenger.new(interface_name: "can0")
145
+ ```
146
+
147
+ You can provide a custom adapter via the `adapter:` option:
148
+
149
+ ```ruby
150
+ my_adapter = MyCustomAdapter.new(interface_name: "can0", logger: Logger.new($stdout))
151
+ messenger = CanMessenger::Messenger.new(interface_name: "can0", adapter: my_adapter)
152
+ ```
153
+
154
+ To build your own adapter, subclass `CanMessenger::Adapter::Base` and implement the required methods
155
+ `open_socket`, `build_can_frame`, `receive_message`, and `parse_frame`.
156
+
119
157
  ## Important Considerations
120
158
 
121
159
  Before using `can_messenger`, please note the following:
122
160
 
123
161
  - **Environment Requirements:**
124
-
125
162
  - **SocketCAN** must be available on your Linux system.
126
163
  - **Permissions:** Working with raw sockets may require elevated privileges or membership in a specific group to open and bind to CAN interfaces without running as root.
127
164
 
128
165
  - **API Changes (v1.0.0 and later):**
129
-
130
166
  - **Keyword Arguments:** The Messenger API now requires keyword arguments. For example, when initializing the Messenger:
131
167
 
132
168
  ```ruby
@@ -150,16 +186,14 @@ Before using `can_messenger`, please note the following:
150
186
  ```
151
187
 
152
188
  - **Threading & Socket Management:**
153
-
154
189
  - **Blocking Behavior:** The gem uses blocking socket calls and continuously listens for messages. Manage the listener’s lifecycle appropriately, especially in multi-threaded environments. Always call `stop_listening` to gracefully shut down the listener.
155
190
  - **Resource Cleanup:** The socket is automatically closed when the listening loop terminates. Stop the listener to avoid resource leaks.
156
191
 
157
192
  - **Logging:**
158
-
159
193
  - **Default Logger:** If no logger is provided, logs go to standard output. Provide a custom logger if you want more control.
160
194
 
161
195
  - **CAN Frame Format Assumptions:**
162
- - 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.
196
+ - By default, the gem uses **native endianness** for CAN IDs (little-endian on most x86/ARM systems). **Changed in v2.0.0:** this default was previously `:big`. You can override this by passing `endianness: :big` or `endianness: :little`.
163
197
  - 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.
164
198
 
165
199
  ## Features
@@ -168,6 +202,7 @@ Before using `can_messenger`, please note the following:
168
202
  - **Receive CAN Messages**: Continuously listen for messages on a CAN interface.
169
203
  - **Filtering**: Optional ID filters for incoming messages (single ID, range, or array).
170
204
  - **Logging**: Logs errors and events for debugging/troubleshooting.
205
+ - **DBC Parsing**: Parse DBC files to encode messages by name and decode incoming frames.
171
206
 
172
207
  ## Development
173
208
 
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanMessenger
4
+ module Adapter
5
+ # Base adapter defines the interface for CAN bus adapters.
6
+ # Concrete adapters must implement all of the methods defined here.
7
+ class Base
8
+ attr_reader :interface_name, :logger, :endianness
9
+
10
+ def self.native_endianness
11
+ native = pack_uint(1, "I")
12
+ return :little if native == pack_uint(1, "V")
13
+ return :big if native == pack_uint(1, "N")
14
+
15
+ # Fallback for unusual platforms.
16
+ native.bytes.first == 1 ? :little : :big
17
+ end
18
+
19
+ def self.pack_uint(value, template)
20
+ [value].pack(template)
21
+ end
22
+ private_class_method :pack_uint
23
+
24
+ def initialize(interface_name:, logger:, endianness: :native)
25
+ @interface_name = interface_name
26
+ @logger = logger
27
+ normalized_endianness = normalize_endianness(endianness)
28
+ @endianness = normalized_endianness == :native ? self.class.native_endianness : normalized_endianness
29
+ end
30
+
31
+ # Open a socket for the underlying interface.
32
+ # @return [Object] adapter-specific socket
33
+ def open_socket(can_fd: false)
34
+ raise NotImplementedError, "open_socket must be implemented in subclasses"
35
+ end
36
+
37
+ # Build a frame ready to be written to the socket.
38
+ def build_can_frame(id:, data:, extended_id: false, can_fd: false)
39
+ raise NotImplementedError, "build_can_frame must be implemented in subclasses"
40
+ end
41
+
42
+ # Receive and parse a frame from the socket.
43
+ def receive_message(socket:, can_fd: false)
44
+ raise NotImplementedError, "receive_message must be implemented in subclasses"
45
+ end
46
+
47
+ # Parse a raw frame string into a message hash.
48
+ def parse_frame(frame:, can_fd: false)
49
+ raise NotImplementedError, "parse_frame must be implemented in subclasses"
50
+ end
51
+
52
+ private
53
+
54
+ def normalize_endianness(endianness)
55
+ normalized = endianness.is_a?(String) ? endianness.strip.downcase.to_sym : endianness
56
+ return normalized if %i[native little big].include?(normalized)
57
+
58
+ raise ArgumentError, "endianness must be :native, :little, or :big"
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require_relative "base"
5
+
6
+ module CanMessenger
7
+ module Adapter
8
+ # Adapter implementation for Linux SocketCAN interfaces.
9
+ class Socketcan < Base
10
+ FRAME_SIZE = 16
11
+ CANFD_FRAME_SIZE = 72
12
+ MIN_FRAME_SIZE = 8
13
+ MAX_FD_DATA = 64
14
+ TIMEOUT = [1, 0].pack("l_2")
15
+
16
+ # Creates and configures a CAN socket bound to the interface.
17
+ def open_socket(can_fd: false)
18
+ socket = Socket.open(Socket::PF_CAN, Socket::SOCK_RAW, Socket::CAN_RAW)
19
+ configure_socket(socket, can_fd: can_fd)
20
+ socket
21
+ rescue StandardError => e
22
+ close_socket(socket)
23
+ logger.error("Error creating CAN socket on interface #{interface_name}: #{e}")
24
+ nil
25
+ end
26
+
27
+ # Builds a raw CAN or CAN FD frame for SocketCAN.
28
+ def build_can_frame(id:, data:, extended_id: false, can_fd: false) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
29
+ if can_fd
30
+ raise ArgumentError, "CAN FD data cannot exceed #{MAX_FD_DATA} bytes" if data.size > MAX_FD_DATA
31
+ elsif data.size > 8
32
+ raise ArgumentError, "CAN data cannot exceed 8 bytes"
33
+ end
34
+
35
+ # Mask the ID to 29 bits
36
+ can_id = id & 0x1FFFFFFF
37
+ # Set bit 31 for extended frames
38
+ can_id |= 0x80000000 if extended_id
39
+
40
+ # Pack the ID based on endianness
41
+ id_bytes = endianness == :big ? [can_id].pack("L>") : [can_id].pack("V")
42
+
43
+ dlc_and_pad = [data.size, 0, 0, 0].pack("C*")
44
+
45
+ payload = if can_fd
46
+ data.pack("C*").ljust(MAX_FD_DATA, "\x00")
47
+ else
48
+ data.pack("C*").ljust(8, "\x00")
49
+ end
50
+
51
+ id_bytes + dlc_and_pad + payload
52
+ end
53
+
54
+ # Reads a frame from the socket and parses it into a hash.
55
+ def receive_message(socket:, can_fd: false)
56
+ frame_size = can_fd ? CANFD_FRAME_SIZE : FRAME_SIZE
57
+ frame = socket.recv(frame_size)
58
+ return nil if frame.nil? || frame.size < MIN_FRAME_SIZE
59
+
60
+ parse_frame(frame: frame, can_fd: can_fd)
61
+ rescue IO::WaitReadable
62
+ nil
63
+ rescue StandardError => e
64
+ logger.error("Error receiving CAN message on interface #{interface_name}: #{e}")
65
+ nil
66
+ end
67
+
68
+ # Parses a raw CAN frame string into a hash with id, data and extended flag.
69
+ def parse_frame(frame:, can_fd: nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
70
+ return nil unless frame && frame.size >= MIN_FRAME_SIZE
71
+
72
+ use_fd = can_fd.nil? ? frame.size >= CANFD_FRAME_SIZE : can_fd
73
+
74
+ raw_id = unpack_frame_id(frame: frame)
75
+ extended = raw_id.anybits?(0x80000000)
76
+ id = raw_id & 0x1FFFFFFF
77
+
78
+ data_length = if use_fd
79
+ frame[4].ord
80
+ else
81
+ frame[4].ord & 0x0F
82
+ end
83
+
84
+ data = if frame.size >= MIN_FRAME_SIZE + data_length
85
+ frame[MIN_FRAME_SIZE, data_length].unpack("C*")
86
+ else
87
+ []
88
+ end
89
+
90
+ { id: id, data: data, extended: extended }
91
+ rescue StandardError => e
92
+ logger.error("Error parsing CAN frame: #{e}")
93
+ nil
94
+ end
95
+
96
+ private
97
+
98
+ def unpack_frame_id(frame:)
99
+ if endianness == :big
100
+ frame[0..3].unpack1("L>")
101
+ else
102
+ frame[0..3].unpack1("V")
103
+ end
104
+ end
105
+
106
+ def configure_socket(socket, can_fd:)
107
+ socket.bind(Socket.pack_sockaddr_can(interface_name))
108
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, TIMEOUT)
109
+ return unless can_fd && Socket.const_defined?(:CAN_RAW_FD_FRAMES)
110
+
111
+ socket.setsockopt(Socket.const_defined?(:SOL_CAN_RAW) ? Socket::SOL_CAN_RAW : Socket::CAN_RAW,
112
+ Socket::CAN_RAW_FD_FRAMES, 1)
113
+ end
114
+
115
+ def close_socket(socket)
116
+ return unless socket
117
+ return if socket.closed?
118
+
119
+ socket.close
120
+ rescue StandardError
121
+ # Ignore close errors so we can report the original failure.
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,515 @@
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
+ return value unless sign == :signed && length.positive?
510
+
511
+ msb_set = (value >> (length - 1)).allbits?(1)
512
+ msb_set ? value - (1 << length) : value
513
+ end
514
+ end
515
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "socket"
4
3
  require "logger"
4
+ require_relative "adapter/socketcan"
5
5
 
6
6
  module CanMessenger
7
7
  # Messenger
@@ -15,25 +15,24 @@ module CanMessenger
15
15
  # messenger.start_listening do |message|
16
16
  # puts "Received: ID=#{message[:id]}, Data=#{message[:data].map { |b| '0x%02X' % b }}"
17
17
  # end
18
- class Messenger # rubocop:disable Metrics/ClassLength
19
- FRAME_SIZE = 16
20
- CANFD_FRAME_SIZE = 72
21
- MIN_FRAME_SIZE = 8
22
- MAX_FD_DATA = 64
23
- TIMEOUT = [1, 0].pack("l_2")
24
-
18
+ class Messenger
25
19
  # Initializes a new Messenger instance.
26
20
  #
27
21
  # @param [String] interface_name The CAN interface to use (e.g., 'can0').
28
22
  # @param [Logger, nil] logger Optional logger for error handling and debug information.
29
- # @param [Symbol] endianness The endianness of the CAN ID (default: :big) can be :big or :little.
23
+ # @param [Symbol] endianness The endianness of the CAN ID (default: :native) can be :big, :little, or :native.
30
24
  # @return [void]
31
- def initialize(interface_name:, logger: nil, endianness: :big, can_fd: false)
25
+ def initialize(interface_name:, logger: nil, endianness: :native, can_fd: false, adapter: Adapter::Socketcan)
32
26
  @interface_name = interface_name
33
27
  @logger = logger || Logger.new($stdout)
34
28
  @listening = true # Control flag for listening loop
35
- @endianness = endianness # :big or :little
29
+ @endianness = endianness # :big, :little, or :native
36
30
  @can_fd = can_fd
31
+ @adapter = if adapter.is_a?(Class)
32
+ adapter.new(interface_name: interface_name, logger: @logger, endianness: endianness)
33
+ else
34
+ adapter
35
+ end
37
36
  end
38
37
 
39
38
  # Sends a CAN message by writing directly to a raw CAN socket
@@ -42,10 +41,12 @@ module CanMessenger
42
41
  # @param [Array<Integer>] data The data bytes of the CAN message (0 to 8 bytes).
43
42
  # @return [void]
44
43
  def send_can_message(id:, data:, extended_id: false, can_fd: nil)
44
+ raise ArgumentError, "id and data are required" if id.nil? || data.nil?
45
+
45
46
  use_fd = can_fd.nil? ? @can_fd : can_fd
46
47
 
47
48
  with_socket(can_fd: use_fd) do |socket|
48
- frame = build_can_frame(id: id, data: data, extended_id: extended_id, can_fd: use_fd)
49
+ frame = @adapter.build_can_frame(id: id, data: data, extended_id: extended_id, can_fd: use_fd)
49
50
  socket.write(frame)
50
51
  end
51
52
  rescue ArgumentError
@@ -54,6 +55,23 @@ module CanMessenger
54
55
  @logger.error("Error sending CAN message (ID: #{id}): #{e}")
55
56
  end
56
57
 
58
+ # Encodes and sends a CAN message using a DBC definition
59
+ #
60
+ # @param [String] message_name The message name to encode
61
+ # @param [Hash] signals Values for each signal in the message
62
+ # @param [CanMessenger::DBC] dbc The DBC instance used for encoding (defaults to @dbc)
63
+ # @return [void]
64
+ def send_dbc_message(message_name:, signals:, dbc: @dbc, extended_id: false, can_fd: nil)
65
+ raise ArgumentError, "dbc is required" if dbc.nil?
66
+
67
+ encoded = dbc.encode_can(message_name, signals)
68
+ send_can_message(id: encoded[:id], data: encoded[:data], extended_id: extended_id, can_fd: can_fd)
69
+ rescue ArgumentError
70
+ raise
71
+ rescue StandardError => e
72
+ @logger.error("Error sending DBC message #{message_name}: #{e}")
73
+ end
74
+
57
75
  # Continuously listens for CAN messages on the specified interface.
58
76
  #
59
77
  # This method listens for incoming CAN messages and applies an optional filter.
@@ -67,7 +85,7 @@ module CanMessenger
67
85
  # - `:id` [Integer] the CAN message ID
68
86
  # - `:data` [Array<Integer>] the message data bytes
69
87
  # @return [void]
70
- def start_listening(filter: nil, can_fd: nil, &block)
88
+ def start_listening(filter: nil, can_fd: nil, dbc: nil, &)
71
89
  return @logger.error("No block provided to handle messages.") unless block_given?
72
90
 
73
91
  @listening = true
@@ -76,7 +94,7 @@ module CanMessenger
76
94
 
77
95
  with_socket(can_fd: use_fd) do |socket|
78
96
  @logger.info("Started listening on #{@interface_name}")
79
- process_message(socket, filter, use_fd, &block) while @listening
97
+ process_message(socket, filter, use_fd, dbc, &) while @listening
80
98
  end
81
99
  end
82
100
 
@@ -96,7 +114,7 @@ module CanMessenger
96
114
  # @yield [socket] An open CAN socket.
97
115
  # @return [void]
98
116
  def with_socket(can_fd: @can_fd)
99
- socket = open_can_socket(can_fd: can_fd)
117
+ socket = @adapter.open_socket(can_fd: can_fd)
100
118
  return @logger.error("Failed to open socket, cannot continue operation.") if socket.nil?
101
119
 
102
120
  yield socket
@@ -104,133 +122,25 @@ module CanMessenger
104
122
  socket&.close
105
123
  end
106
124
 
107
- # Creates and configures a CAN socket bound to @interface_name.
108
- #
109
- # @return [Socket, nil] The configured CAN socket, or nil if the socket cannot be opened.
110
- def open_can_socket(can_fd: @can_fd) # rubocop:disable Metrics/MethodLength
111
- socket = Socket.open(Socket::PF_CAN, Socket::SOCK_RAW, Socket::CAN_RAW)
112
- socket.bind(Socket.pack_sockaddr_can(@interface_name))
113
- socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, TIMEOUT)
114
- if can_fd && Socket.const_defined?(:CAN_RAW_FD_FRAMES)
115
- socket.setsockopt(Socket.const_defined?(:SOL_CAN_RAW) ? Socket::SOL_CAN_RAW : Socket::CAN_RAW,
116
- Socket::CAN_RAW_FD_FRAMES, 1)
117
- end
118
- socket
119
- rescue StandardError => e
120
- @logger.error("Error creating CAN socket on interface #{@interface_name}: #{e}")
121
- nil
122
- end
123
-
124
- # Builds a raw CAN or CAN FD frame for SocketCAN.
125
- #
126
- # @param id [Integer] the CAN ID
127
- # @param data [Array<Integer>] data bytes (up to 8 for classic, 64 for CAN FD)
128
- # @param can_fd [Boolean] whether to build a CAN FD frame
129
- # @return [String] the packed CAN frame
130
- def build_can_frame(id:, data:, extended_id: false, can_fd: false) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
131
- if can_fd
132
- raise ArgumentError, "CAN FD data cannot exceed #{MAX_FD_DATA} bytes" if data.size > MAX_FD_DATA
133
- elsif data.size > 8
134
- raise ArgumentError, "CAN data cannot exceed 8 bytes"
135
- end
136
-
137
- # Mask the ID to 29 bits
138
- can_id = id & 0x1FFFFFFF
139
-
140
- # If extended_id == true, set bit 31 (CAN_EFF_FLAG)
141
- can_id |= 0x80000000 if extended_id
142
-
143
- # Pack the 4‐byte ID (big-endian or little-endian)
144
- id_bytes = @endianness == :big ? [can_id].pack("L>") : [can_id].pack("V")
145
-
146
- # 1 byte for DLC/length, then 3 bytes for flags/reserved
147
- dlc_and_pad = [data.size, 0, 0, 0].pack("C*")
148
-
149
- payload = if can_fd
150
- data.pack("C*").ljust(MAX_FD_DATA, "\x00")
151
- else
152
- data.pack("C*").ljust(8, "\x00")
153
- end
154
-
155
- id_bytes + dlc_and_pad + payload
156
- end
157
-
158
125
  # Processes a single CAN message from `socket`. Applies filter, yields to block if it matches.
159
126
  #
160
127
  # @param socket [Socket] The CAN socket.
161
128
  # @param filter [Integer, Range, Array<Integer>, nil] Optional filter for CAN IDs.
162
129
  # @yield [message] Yields the message if it passes filtering.
163
130
  # @return [void]
164
- def process_message(socket, filter, can_fd)
165
- message = receive_message(socket: socket, can_fd: can_fd)
131
+ def process_message(socket, filter, can_fd, dbc, &block)
132
+ message = @adapter.receive_message(socket: socket, can_fd: can_fd)
166
133
  return if message.nil?
167
134
  return if filter && !matches_filter?(message_id: message[:id], filter: filter)
168
135
 
169
- yield(message)
170
- rescue StandardError => e
171
- @logger.error("Unexpected error in listening loop: #{e.message}")
172
- end
173
-
174
- # Reads a frame from the socket and parses it into { id:, data: }, or nil if none is received.
175
- #
176
- # @param socket [Socket]
177
- # @return [Hash, nil]
178
- def receive_message(socket:, can_fd: false)
179
- frame_size = can_fd ? CANFD_FRAME_SIZE : FRAME_SIZE
180
- frame = socket.recv(frame_size)
181
- return nil if frame.nil? || frame.size < MIN_FRAME_SIZE
182
-
183
- parse_frame(frame: frame, can_fd: can_fd)
184
- rescue IO::WaitReadable
185
- nil
186
- rescue StandardError => e
187
- @logger.error("Error receiving CAN message on interface #{@interface_name}: #{e}")
188
- nil
189
- end
190
-
191
- # Parses a raw CAN frame into { id: Integer, data: Array<Integer> }, or nil on error.
192
- #
193
- # @param [String] frame
194
- # @return [Hash, nil]
195
- def parse_frame(frame:, can_fd: nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
196
- return nil unless frame && frame.size >= MIN_FRAME_SIZE
197
-
198
- use_fd = can_fd.nil? ? frame.size >= CANFD_FRAME_SIZE : can_fd
199
-
200
- raw_id = unpack_frame_id(frame: frame)
201
-
202
- # Determine if EFF bit is set
203
- extended = raw_id.anybits?(0x80000000)
204
- # or raw_id.anybits?(0x80000000) if your Ruby version supports `Integer#anybits?`
205
-
206
- # Now mask off everything except the lower 29 bits
207
- id = raw_id & 0x1FFFFFFF
208
-
209
- data_length = if use_fd
210
- frame[4].ord
211
- else
212
- frame[4].ord & 0x0F
213
- end
214
-
215
- # Extract data
216
- data = if frame.size >= MIN_FRAME_SIZE + data_length
217
- frame[MIN_FRAME_SIZE, data_length].unpack("C*")
218
- else
219
- []
220
- end
136
+ if dbc
137
+ decoded = dbc.decode_can(message[:id], message[:data])
138
+ message[:decoded] = decoded if decoded
139
+ end
221
140
 
222
- { id: id, data: data, extended: extended }
141
+ block.call(message)
223
142
  rescue StandardError => e
224
- @logger.error("Error parsing CAN frame: #{e}")
225
- nil
226
- end
227
-
228
- def unpack_frame_id(frame:)
229
- if @endianness == :big
230
- frame[0..3].unpack1("L>")
231
- else
232
- frame[0..3].unpack1("V")
233
- end
143
+ @logger.error("Unexpected error in listening loop: #{e.message}")
234
144
  end
235
145
 
236
146
  # Checks whether the given message ID matches the specified filter.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CanMessenger
4
- VERSION = "1.3.0"
4
+ VERSION = "2.0.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,13 @@
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: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - fk1018
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-07-08 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies: []
13
12
  description: CanMessenger provides an interface to send and receive messages over
14
13
  the CAN bus, useful for applications requiring CAN communication in Ruby.
@@ -18,8 +17,13 @@ executables: []
18
17
  extensions: []
19
18
  extra_rdoc_files: []
20
19
  files:
20
+ - CHANGELOG.md
21
+ - LICENSE.txt
21
22
  - README.md
22
23
  - lib/can_messenger.rb
24
+ - lib/can_messenger/adapter/base.rb
25
+ - lib/can_messenger/adapter/socketcan.rb
26
+ - lib/can_messenger/dbc.rb
23
27
  - lib/can_messenger/messenger.rb
24
28
  - lib/can_messenger/version.rb
25
29
  homepage: https://github.com/fk1018/can_messenger
@@ -31,7 +35,6 @@ metadata:
31
35
  source_code_uri: https://github.com/fk1018/can_messenger
32
36
  changelog_uri: https://github.com/fk1018/can_messenger/blob/main/CHANGELOG.md
33
37
  rubygems_mfa_required: 'true'
34
- post_install_message:
35
38
  rdoc_options: []
36
39
  require_paths:
37
40
  - lib
@@ -39,15 +42,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
39
42
  requirements:
40
43
  - - ">="
41
44
  - !ruby/object:Gem::Version
42
- version: 3.0.0
45
+ version: 4.0.1
43
46
  required_rubygems_version: !ruby/object:Gem::Requirement
44
47
  requirements:
45
48
  - - ">="
46
49
  - !ruby/object:Gem::Version
47
50
  version: '0'
48
51
  requirements: []
49
- rubygems_version: 3.5.22
50
- signing_key:
52
+ rubygems_version: 4.0.3
51
53
  specification_version: 4
52
54
  summary: A simple Ruby wrapper to read and write CAN bus messages.
53
55
  test_files: []