can_messenger 1.4.0 → 2.1.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: d6460da6eaff67f984db1e7f63466b8350140b244c4e7beaea56595a95e7da92
4
- data.tar.gz: 7f06c6f54c0e6fc7ea49aff994ad7ad076c7d3a5f98cb50554793751f8941372
3
+ metadata.gz: b79b0febdca3de5f068c142413eef6f959461518e3928cd9beed52b14ee8ceef
4
+ data.tar.gz: aca5cf8fde09f75b6f0d7a81ab1bca9e6a59fc2d3d811a72321eb6b9d6b88ba9
5
5
  SHA512:
6
- metadata.gz: b1b3bdb759e1ed223c0e08d43c7fdcbf556123c007ecad5dce1b2b4b9b8ac35af289b64a15d16c46939a6ee9028d52b5015f35f4576b14637406884e9e881270
7
- data.tar.gz: 1de19b416ecf4733f074c7c704a966313a86c6f16d9a631f7ff14a5b3ebf9b923dfe24930948bbb910554a376e168f49ea1a6c5c8abd324902efbdc9ed65366d
6
+ metadata.gz: 864bd3e56da612dee5e02bd6e575eff32adab311c3090e2a874704ee7fb6f84895146294a83362be40faef5477e3951c69203f6ecd4ad094a79067d4a5928d1e
7
+ data.tar.gz: c1bc154e2b42b3373aadd0e27572d66221d3859fb5272f0594fc5d1adb1a4e1382de1e04e02e7e2171bf28b6f2a1cfc33500b4c4af0f0acd3edf35ea3b87c1f4
data/CHANGELOG.md ADDED
@@ -0,0 +1,181 @@
1
+ ## [Unreleased]
2
+
3
+ ## [2.1.0] - 2026-02-23
4
+
5
+ ### Changed
6
+
7
+ - Promote the first stable docs snapshot version to `2.1.0`.
8
+ - Tighten DBC signal bounds validation and report exact out-of-range bit positions.
9
+
10
+ ### Fixed
11
+
12
+ - Fix DBC big-endian (`@0`) bit mapping for multi-byte signals.
13
+ - Reject unsigned DBC signal values that exceed the field's bit-length range instead of silently wrapping.
14
+ - Align `Messenger` RBS signatures with runtime behavior (`adapter:` support and correct private method surface).
15
+ - Update DBC specs to cover the corrected big-endian behavior and unsigned overflow errors.
16
+
17
+ ## [2.0.0] - 2026-02-02
18
+
19
+ ### Changed
20
+
21
+ - **Breaking:** Require Ruby 4.0.1 or higher.
22
+ - Update CI to run on Ruby 4.0.1.
23
+ - Upgrade RuboCop to support Ruby 4.0 and refresh linting.
24
+ - Minor style cleanups in DBC parsing and message listener block forwarding.
25
+ - Extract SocketCAN logic into a dedicated adapter and add a base adapter interface.
26
+ - Allow injecting a custom adapter into `Messenger` for alternate transports or testing.
27
+ - **Breaking:** Default CAN ID endianness is now native (`:native`) instead of `:big`.
28
+
29
+ ### Fixed
30
+
31
+ - Close SocketCAN sockets when bind/setsockopt fails to avoid leaks.
32
+
33
+ ## [1.4.0] - 2025-07-25
34
+
35
+ ### Added
36
+
37
+ - `send_dbc_message` helper for encoding and sending messages defined in DBC files.
38
+
39
+ ### Changed
40
+
41
+ - `send_can_message` now only accepts raw frame parameters.
42
+ - DBC parsing code split into helper methods for clarity.
43
+
44
+ ### Fixed
45
+
46
+ - Correct encoding of negative signal values using two's-complement.
47
+
48
+ ## [1.3.0] - 2025-06-27
49
+
50
+ ### Added
51
+
52
+ - Optional **CAN FD** support for sending and receiving up to 64-byte frames.
53
+
54
+ ### Changed
55
+
56
+ - Updated APIs to accept a `can_fd:` flag on initialization and message methods.
57
+
58
+ ### Fixed
59
+
60
+ - (Nothing since last release.)
61
+
62
+ ## [1.2.1] - 2025-06-05
63
+
64
+ ### Changed
65
+
66
+ - `send_can_message` now raises `ArgumentError` when data length exceeds eight bytes.
67
+ - Updated RBS `initialize` signature to include the `endianness` argument.
68
+ - Fixed formatting in README around extended CAN frames.
69
+ - Clarified spec helper comment.
70
+
71
+ ### Fixed
72
+
73
+ - Addressed a listener restart bug allowing `start_listening` to be called again.
74
+
75
+ ## [1.2.0] - 2025-02-28
76
+
77
+ ### Added
78
+
79
+ - **Explicit extended CAN ID support**.
80
+ - 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.
81
+ - Updated `parse_frame` to detect and report `extended: true` when the EFF bit is set in incoming frames.
82
+ - Added corresponding tests for sending and receiving extended CAN frames.
83
+
84
+ ### Changed
85
+
86
+ - _No breaking changes_, but internal refactoring around how CAN IDs are packed and unpacked.
87
+ - Removed the masking of bit 31 in `unpack_frame_id`, ensuring extended frames are no longer silently treated as standard frames.
88
+
89
+ ### Fixed
90
+
91
+ - (Nothing since last release.)
92
+
93
+ ## [1.1.0] - 2025-02-10
94
+
95
+ ### Changed
96
+
97
+ - **Removed dependency on `cansend`**. We now write CAN frames directly via raw sockets.
98
+ - Internal refactoring to support raw-socket–based sending without changing the public API.
99
+
100
+ ### Fixed
101
+
102
+ ## [1.0.3] - 2025-02-09
103
+
104
+ - Revert release.yml
105
+
106
+ ### Fixed
107
+
108
+ ## [1.0.2] - 2025-02-09
109
+
110
+ - Bugfix release.yml
111
+
112
+ ## [1.0.1] - 2025-02-09
113
+
114
+ ### Changed
115
+
116
+ - 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.
117
+ - Made minor documentation clarifications and tweaks to help users avoid common pitfalls.
118
+
119
+ ## [1.0.0] - 2025-02-09
120
+
121
+ ### Changed
122
+
123
+ - **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.
124
+ - **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.
125
+ - Refactored `start_listening`.
126
+ - Enhanced error handling throughout the gem with more detailed logging.
127
+ - Updated type signatures (RBS) and documentation to match the new API.
128
+ - Refactored tests to reflect the new API and improved error handling.
129
+
130
+ ## [0.2.3] - 2025-02-01
131
+
132
+ ### Changed
133
+
134
+ - Updated the internal listening loop in `start_listening` to continue iterating on nil (timeout) responses instead of breaking out, improving reliability.
135
+ - Suppressed log output during tests by injecting a silent logger.
136
+ - Updated the test suite to better handle long-running listening loops and error conditions.
137
+
138
+ ## [0.2.2] - 2024-12-06
139
+
140
+ ### Changed
141
+
142
+ - Updated README.md to reflect modern debian package install command.
143
+
144
+ ## [0.2.1] - 2024-12-06
145
+
146
+ ### Changed
147
+
148
+ - Updated `start_listening` RBS signature to include the `filter` parameter, ensuring type definitions match the implementation.
149
+
150
+ ## [0.2.0] - 2024-12-05
151
+
152
+ ### Added
153
+
154
+ - Filtering support for `start_listening` via a `filter` parameter:
155
+ - Single CAN ID.
156
+ - Range of CAN IDs.
157
+ - Array of CAN IDs.
158
+
159
+ ### Changed
160
+
161
+ - Refactored `start_listening` to support optional filtering of incoming CAN messages.
162
+ - Documentation updates for `start_listening` in README.
163
+
164
+ ## [0.1.0] - 2024-11-10
165
+
166
+ - Initial release
167
+ [Unreleased]: https://github.com/fk1018/can_messenger/compare/v2.1.0...HEAD
168
+ [2.1.0]: https://github.com/fk1018/can_messenger/compare/v2.0.0...v2.1.0
169
+ [2.0.0]: https://github.com/fk1018/can_messenger/compare/v1.3.0...v2.0.0
170
+ [1.3.0]: https://github.com/fk1018/can_messenger/compare/v1.2.1...v1.3.0
171
+ [1.2.1]: https://github.com/fk1018/can_messenger/compare/v1.2.0...v1.2.1
172
+ [1.2.0]: https://github.com/fk1018/can_messenger/compare/v1.1.0...v1.2.0
173
+ [1.1.0]: https://github.com/fk1018/can_messenger/compare/v1.0.3...v1.1.0
174
+ [1.0.3]: https://github.com/fk1018/can_messenger/compare/v1.0.1...v1.0.3
175
+ [1.0.1]: https://github.com/fk1018/can_messenger/compare/v1.0.0...v1.0.1
176
+ [1.0.0]: https://github.com/fk1018/can_messenger/compare/v0.2.3...v1.0.0
177
+ [0.2.3]: https://github.com/fk1018/can_messenger/compare/v0.2.2...v0.2.3
178
+ [0.2.2]: https://github.com/fk1018/can_messenger/compare/v0.2.1...v0.2.2
179
+ [0.2.1]: https://github.com/fk1018/can_messenger/compare/v0.2.0...v0.2.1
180
+ [0.2.0]: https://github.com/fk1018/can_messenger/compare/v0.1.0...v0.2.0
181
+ [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
@@ -12,7 +12,8 @@
12
12
 
13
13
  ## Requirements
14
14
 
15
- - Ruby 3.0 or higher.
15
+ - Ruby 4.0.1 or higher.
16
+ - Docker (optional, for containerized development without installing Ruby locally).
16
17
 
17
18
  ## Installation
18
19
 
@@ -135,17 +136,34 @@ To stop listening, use:
135
136
  messenger.stop_listening
136
137
  ```
137
138
 
139
+ ### Adapters
140
+
141
+ `CanMessenger::Messenger` delegates low-level CAN bus operations to an adapter. By default it uses the
142
+ SocketCAN adapter which communicates with Linux CAN interfaces using raw sockets:
143
+
144
+ ```ruby
145
+ messenger = CanMessenger::Messenger.new(interface_name: "can0")
146
+ ```
147
+
148
+ You can provide a custom adapter via the `adapter:` option:
149
+
150
+ ```ruby
151
+ my_adapter = MyCustomAdapter.new(interface_name: "can0", logger: Logger.new($stdout))
152
+ messenger = CanMessenger::Messenger.new(interface_name: "can0", adapter: my_adapter)
153
+ ```
154
+
155
+ To build your own adapter, subclass `CanMessenger::Adapter::Base` and implement the required methods
156
+ `open_socket`, `build_can_frame`, `receive_message`, and `parse_frame`.
157
+
138
158
  ## Important Considerations
139
159
 
140
160
  Before using `can_messenger`, please note the following:
141
161
 
142
162
  - **Environment Requirements:**
143
-
144
163
  - **SocketCAN** must be available on your Linux system.
145
164
  - **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.
146
165
 
147
166
  - **API Changes (v1.0.0 and later):**
148
-
149
167
  - **Keyword Arguments:** The Messenger API now requires keyword arguments. For example, when initializing the Messenger:
150
168
 
151
169
  ```ruby
@@ -169,16 +187,14 @@ Before using `can_messenger`, please note the following:
169
187
  ```
170
188
 
171
189
  - **Threading & Socket Management:**
172
-
173
190
  - **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.
174
191
  - **Resource Cleanup:** The socket is automatically closed when the listening loop terminates. Stop the listener to avoid resource leaks.
175
192
 
176
193
  - **Logging:**
177
-
178
194
  - **Default Logger:** If no logger is provided, logs go to standard output. Provide a custom logger if you want more control.
179
195
 
180
196
  - **CAN Frame Format Assumptions:**
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.
197
+ - 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`.
182
198
  - 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.
183
199
 
184
200
  ## Features
@@ -191,7 +207,58 @@ Before using `can_messenger`, please note the following:
191
207
 
192
208
  ## Development
193
209
 
194
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test:rspec` to execute the test suite.
210
+ ### Docker-first workflow (no local Ruby required)
211
+
212
+ Build the development image:
213
+
214
+ ```bash
215
+ docker compose build app
216
+ ```
217
+
218
+ Run RuboCop:
219
+
220
+ ```bash
221
+ docker compose run --rm lint
222
+ ```
223
+
224
+ Run the test suite:
225
+
226
+ ```bash
227
+ docker compose run --rm test
228
+ ```
229
+
230
+ Build the gem:
231
+
232
+ ```bash
233
+ docker compose run --rm build
234
+ ```
235
+
236
+ Install docs dependencies (Docusaurus):
237
+
238
+ ```bash
239
+ docker compose run --rm docs npm ci
240
+ ```
241
+
242
+ Build the docs site:
243
+
244
+ ```bash
245
+ docker compose run --rm docs npm run build
246
+ ```
247
+
248
+ Preview docs locally:
249
+
250
+ ```bash
251
+ docker compose run --rm --service-ports docs npm run start
252
+ ```
253
+
254
+ ### Local Ruby workflow
255
+
256
+ If you already have Ruby installed locally, you can still use:
257
+
258
+ ```bash
259
+ bin/setup
260
+ bundle exec rake test:rspec
261
+ ```
195
262
 
196
263
  ## Contributing
197
264
 
@@ -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
@@ -85,7 +85,7 @@ module CanMessenger
85
85
  # @param [Message] _current The current message being processed (unused but kept for API consistency)
86
86
  # @return [Signal, nil] A Signal object if the line matches, nil otherwise
87
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*\(([^,]+),([^\)]+)\)/))
88
+ return unless (m = line.match(/^SG_\s+(\w+)\s*:\s*(\d+)\|(\d+)@(\d)([+-])\s*\(([^,]+),([^)]+)\)/))
89
89
 
90
90
  sig_name = m[1]
91
91
  start_bit = m[2].to_i
@@ -283,15 +283,15 @@ module CanMessenger
283
283
  # @return [void]
284
284
  # @raise [ArgumentError] If signal bits exceed message boundaries or start_bit is negative
285
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
286
  raise ArgumentError, "Signal #{name}: start_bit (#{start_bit}) cannot be negative" if start_bit.negative?
287
+ raise ArgumentError, "Signal #{name}: length (#{length}) must be positive" if length <= 0
290
288
 
291
- return unless max_bit > max_allowed_bit
289
+ max_allowed_bit = max_allowed_bit_for(message_size_bytes)
290
+ invalid_position = invalid_bit_position(max_allowed_bit)
291
+ return unless invalid_position
292
292
 
293
293
  raise ArgumentError,
294
- "Signal #{name}: signal bits #{start_bit}..#{max_bit} exceed message size " \
294
+ "Signal #{name}: bit position #{invalid_position} exceeds message size " \
295
295
  "(#{message_size_bytes} bytes = #{max_allowed_bit + 1} bits)"
296
296
  end
297
297
 
@@ -328,9 +328,15 @@ module CanMessenger
328
328
  # @return [void]
329
329
  # @raise [ArgumentError] If an unsigned value is negative
330
330
  def validate_unsigned_value(raw)
331
- return unless sign == :unsigned && raw.negative?
331
+ return unless sign == :unsigned
332
+
333
+ raise ArgumentError, "Unsigned value cannot be negative: #{raw}" if raw.negative?
332
334
 
333
- raise ArgumentError, "Unsigned value cannot be negative: #{raw}"
335
+ max_val = (1 << length) - 1
336
+ return if raw <= max_val
337
+
338
+ raise ArgumentError,
339
+ "Unsigned value #{raw} out of range [0..#{max_val}] for #{length}-bit field"
334
340
  end
335
341
 
336
342
  # Validates signed values to ensure they fit in the signal's bit range.
@@ -396,19 +402,12 @@ module CanMessenger
396
402
  if endianness == :little
397
403
  start_bit + bit_offset
398
404
  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
405
+ # DBC big-endian (`@0`) start_bit is in sawtooth numbering and points at the
406
+ # signal MSB. Convert to sequential network numbering, move toward the LSB,
407
+ # then convert back to the little-endian bit index used by byte writes.
408
+ network_start = sawtooth_to_network_bitnum(start_bit)
409
+ network_bit = network_start + (length - 1 - bit_offset)
410
+ sawtooth_to_network_bitnum(network_bit)
412
411
  end
413
412
  end
414
413
 
@@ -506,11 +505,27 @@ module CanMessenger
506
505
  # @param [Integer] value The unsigned integer value to potentially convert
507
506
  # @return [Integer] The final signed or unsigned value
508
507
  def convert_to_signed_if_needed(value)
509
- if sign == :signed && value[length - 1] == 1
510
- value - (1 << length)
511
- else
512
- value
508
+ return value unless sign == :signed && length.positive?
509
+
510
+ msb_set = (value >> (length - 1)).allbits?(1)
511
+ msb_set ? value - (1 << length) : value
512
+ end
513
+
514
+ def sawtooth_to_network_bitnum(bitnum)
515
+ (8 * (bitnum / 8)) + (7 - (bitnum % 8))
516
+ end
517
+
518
+ def invalid_bit_position(max_allowed_bit)
519
+ length.times do |i|
520
+ bit_pos = calculate_bit_position(i)
521
+ return bit_pos if bit_pos.negative? || bit_pos > max_allowed_bit
513
522
  end
523
+
524
+ nil
525
+ end
526
+
527
+ def max_allowed_bit_for(message_size_bytes)
528
+ (message_size_bytes * 8) - 1
514
529
  end
515
530
  end
516
531
  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
@@ -47,7 +46,7 @@ module CanMessenger
47
46
  use_fd = can_fd.nil? ? @can_fd : can_fd
48
47
 
49
48
  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)
49
+ frame = @adapter.build_can_frame(id: id, data: data, extended_id: extended_id, can_fd: use_fd)
51
50
  socket.write(frame)
52
51
  end
53
52
  rescue ArgumentError
@@ -86,7 +85,7 @@ module CanMessenger
86
85
  # - `:id` [Integer] the CAN message ID
87
86
  # - `:data` [Array<Integer>] the message data bytes
88
87
  # @return [void]
89
- def start_listening(filter: nil, can_fd: nil, dbc: nil, &block)
88
+ def start_listening(filter: nil, can_fd: nil, dbc: nil, &)
90
89
  return @logger.error("No block provided to handle messages.") unless block_given?
91
90
 
92
91
  @listening = true
@@ -95,7 +94,7 @@ module CanMessenger
95
94
 
96
95
  with_socket(can_fd: use_fd) do |socket|
97
96
  @logger.info("Started listening on #{@interface_name}")
98
- process_message(socket, filter, use_fd, dbc, &block) while @listening
97
+ process_message(socket, filter, use_fd, dbc, &) while @listening
99
98
  end
100
99
  end
101
100
 
@@ -115,7 +114,7 @@ module CanMessenger
115
114
  # @yield [socket] An open CAN socket.
116
115
  # @return [void]
117
116
  def with_socket(can_fd: @can_fd)
118
- socket = open_can_socket(can_fd: can_fd)
117
+ socket = @adapter.open_socket(can_fd: can_fd)
119
118
  return @logger.error("Failed to open socket, cannot continue operation.") if socket.nil?
120
119
 
121
120
  yield socket
@@ -123,57 +122,6 @@ module CanMessenger
123
122
  socket&.close
124
123
  end
125
124
 
126
- # Creates and configures a CAN socket bound to @interface_name.
127
- #
128
- # @return [Socket, nil] The configured CAN socket, or nil if the socket cannot be opened.
129
- def open_can_socket(can_fd: @can_fd) # rubocop:disable Metrics/MethodLength
130
- socket = Socket.open(Socket::PF_CAN, Socket::SOCK_RAW, Socket::CAN_RAW)
131
- socket.bind(Socket.pack_sockaddr_can(@interface_name))
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
137
- socket
138
- rescue StandardError => e
139
- @logger.error("Error creating CAN socket on interface #{@interface_name}: #{e}")
140
- nil
141
- end
142
-
143
- # Builds a raw CAN or CAN FD frame for SocketCAN.
144
- #
145
- # @param id [Integer] the CAN ID
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
155
-
156
- # Mask the ID to 29 bits
157
- can_id = id & 0x1FFFFFFF
158
-
159
- # If extended_id == true, set bit 31 (CAN_EFF_FLAG)
160
- can_id |= 0x80000000 if extended_id
161
-
162
- # Pack the 4‐byte ID (big-endian or little-endian)
163
- id_bytes = @endianness == :big ? [can_id].pack("L>") : [can_id].pack("V")
164
-
165
- # 1 byte for DLC/length, then 3 bytes for flags/reserved
166
- dlc_and_pad = [data.size, 0, 0, 0].pack("C*")
167
-
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
173
-
174
- id_bytes + dlc_and_pad + payload
175
- end
176
-
177
125
  # Processes a single CAN message from `socket`. Applies filter, yields to block if it matches.
178
126
  #
179
127
  # @param socket [Socket] The CAN socket.
@@ -181,7 +129,7 @@ module CanMessenger
181
129
  # @yield [message] Yields the message if it passes filtering.
182
130
  # @return [void]
183
131
  def process_message(socket, filter, can_fd, dbc, &block)
184
- message = receive_message(socket: socket, can_fd: can_fd)
132
+ message = @adapter.receive_message(socket: socket, can_fd: can_fd)
185
133
  return if message.nil?
186
134
  return if filter && !matches_filter?(message_id: message[:id], filter: filter)
187
135
 
@@ -195,68 +143,6 @@ module CanMessenger
195
143
  @logger.error("Unexpected error in listening loop: #{e.message}")
196
144
  end
197
145
 
198
- # Reads a frame from the socket and parses it into { id:, data: }, or nil if none is received.
199
- #
200
- # @param socket [Socket]
201
- # @return [Hash, nil]
202
- def receive_message(socket:, can_fd: false)
203
- frame_size = can_fd ? CANFD_FRAME_SIZE : FRAME_SIZE
204
- frame = socket.recv(frame_size)
205
- return nil if frame.nil? || frame.size < MIN_FRAME_SIZE
206
-
207
- parse_frame(frame: frame, can_fd: can_fd)
208
- rescue IO::WaitReadable
209
- nil
210
- rescue StandardError => e
211
- @logger.error("Error receiving CAN message on interface #{@interface_name}: #{e}")
212
- nil
213
- end
214
-
215
- # Parses a raw CAN frame into { id: Integer, data: Array<Integer> }, or nil on error.
216
- #
217
- # @param [String] frame
218
- # @return [Hash, nil]
219
- def parse_frame(frame:, can_fd: nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
220
- return nil unless frame && frame.size >= MIN_FRAME_SIZE
221
-
222
- use_fd = can_fd.nil? ? frame.size >= CANFD_FRAME_SIZE : can_fd
223
-
224
- raw_id = unpack_frame_id(frame: frame)
225
-
226
- # Determine if EFF bit is set
227
- extended = raw_id.anybits?(0x80000000)
228
- # or raw_id.anybits?(0x80000000) if your Ruby version supports `Integer#anybits?`
229
-
230
- # Now mask off everything except the lower 29 bits
231
- id = raw_id & 0x1FFFFFFF
232
-
233
- data_length = if use_fd
234
- frame[4].ord
235
- else
236
- frame[4].ord & 0x0F
237
- end
238
-
239
- # Extract data
240
- data = if frame.size >= MIN_FRAME_SIZE + data_length
241
- frame[MIN_FRAME_SIZE, data_length].unpack("C*")
242
- else
243
- []
244
- end
245
-
246
- { id: id, data: data, extended: extended }
247
- rescue StandardError => e
248
- @logger.error("Error parsing CAN frame: #{e}")
249
- nil
250
- end
251
-
252
- def unpack_frame_id(frame:)
253
- if @endianness == :big
254
- frame[0..3].unpack1("L>")
255
- else
256
- frame[0..3].unpack1("V")
257
- end
258
- end
259
-
260
146
  # Checks whether the given message ID matches the specified filter.
261
147
  #
262
148
  # @param message_id [Integer] The ID of the incoming CAN message.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CanMessenger
4
- VERSION = "1.4.0"
4
+ VERSION = "2.1.0"
5
5
  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.4.0
4
+ version: 2.1.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-29 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,12 @@ 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
23
26
  - lib/can_messenger/dbc.rb
24
27
  - lib/can_messenger/messenger.rb
25
28
  - lib/can_messenger/version.rb
@@ -32,7 +35,6 @@ metadata:
32
35
  source_code_uri: https://github.com/fk1018/can_messenger
33
36
  changelog_uri: https://github.com/fk1018/can_messenger/blob/main/CHANGELOG.md
34
37
  rubygems_mfa_required: 'true'
35
- post_install_message:
36
38
  rdoc_options: []
37
39
  require_paths:
38
40
  - lib
@@ -40,15 +42,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
40
42
  requirements:
41
43
  - - ">="
42
44
  - !ruby/object:Gem::Version
43
- version: 3.0.0
45
+ version: 4.0.1
44
46
  required_rubygems_version: !ruby/object:Gem::Requirement
45
47
  requirements:
46
48
  - - ">="
47
49
  - !ruby/object:Gem::Version
48
50
  version: '0'
49
51
  requirements: []
50
- rubygems_version: 3.5.22
51
- signing_key:
52
+ rubygems_version: 4.0.3
52
53
  specification_version: 4
53
54
  summary: A simple Ruby wrapper to read and write CAN bus messages.
54
55
  test_files: []