can_messenger 2.0.0 → 2.2.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: 15f09405e67a906115972dc84790769875d1dcdb5cbd78a57827ccdf910d7e05
4
- data.tar.gz: faa23fc30c89b5a81875ff2664e5f97cac2970cb6161599887a1c02da9b5f36c
3
+ metadata.gz: 51737c6d0bee83617a3cb2c68b634f63a65ae5b1c711a4720d7a5a37a08a0612
4
+ data.tar.gz: aebcbd5b0904849633ce3326f60bf9fd28ca47f8c1982601d25161ec383f3a05
5
5
  SHA512:
6
- metadata.gz: bb33fdda392ca2acaea5357a661b5b9dda245c41db5b000802e4d12886d58b16c49d5238312aa254f062fdc9f46c9f05b9f140f202a0ad73e1ceda9b81d397ca
7
- data.tar.gz: 4823263138efc7d61756d473e2e6235487921f7d8e6d8350260dcd80d491bee6927cbf5606671a242576fdb3f3fb5437212f77c008856c3bfa1d2b9a5b5d4924
6
+ metadata.gz: f6d8a36f95f8afcc859d25bd48b5c49b878b7fd1e2e51245ebe142e83dc3e05bc0e2572233da563ec470b0e0b52b3b59ee55c50c7165dc46b7d13302d2df465c
7
+ data.tar.gz: 88bca2b12015fa26df773080f8ad540ec30967b486e6a878afe4319c93b743eb758c6dcef9fe302b2af3ee5776e162129a49236368f31ca1343cc30a117096fc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [2.2.0] - 2026-03-11
4
+
5
+ ### Changed
6
+
7
+ - Make the Ruby Docker workflow install gems against the checked-in lockfile and self-heal stale bundle cache volumes.
8
+ - Promote the docs site stable snapshot to `2.2.0` and add global RubyGems navigation links.
9
+
10
+ ### Fixed
11
+
12
+ - Respect DBC-defined extended CAN IDs when sending messages and when decoding received extended frames.
13
+ - Reject invalid outbound CAN IDs instead of silently coercing them to a different wire value.
14
+
15
+ ## [2.1.0] - 2026-02-23
16
+
17
+ ### Changed
18
+
19
+ - Promote the first stable docs snapshot version to `2.1.0`.
20
+ - Tighten DBC signal bounds validation and report exact out-of-range bit positions.
21
+
22
+ ### Fixed
23
+
24
+ - Fix DBC big-endian (`@0`) bit mapping for multi-byte signals.
25
+ - Reject unsigned DBC signal values that exceed the field's bit-length range instead of silently wrapping.
26
+ - Align `Messenger` RBS signatures with runtime behavior (`adapter:` support and correct private method surface).
27
+ - Update DBC specs to cover the corrected big-endian behavior and unsigned overflow errors.
28
+
3
29
  ## [2.0.0] - 2026-02-02
4
30
 
5
31
  ### Changed
@@ -150,6 +176,9 @@
150
176
  ## [0.1.0] - 2024-11-10
151
177
 
152
178
  - Initial release
179
+ [Unreleased]: https://github.com/fk1018/can_messenger/compare/v2.2.0...HEAD
180
+ [2.2.0]: https://github.com/fk1018/can_messenger/compare/v2.1.0...v2.2.0
181
+ [2.1.0]: https://github.com/fk1018/can_messenger/compare/v2.0.0...v2.1.0
153
182
  [2.0.0]: https://github.com/fk1018/can_messenger/compare/v1.3.0...v2.0.0
154
183
  [1.3.0]: https://github.com/fk1018/can_messenger/compare/v1.2.1...v1.3.0
155
184
  [1.2.1]: https://github.com/fk1018/can_messenger/compare/v1.2.0...v1.2.1
data/README.md CHANGED
@@ -13,6 +13,7 @@
13
13
  ## Requirements
14
14
 
15
15
  - Ruby 4.0.1 or higher.
16
+ - Docker (optional, for containerized development without installing Ruby locally).
16
17
 
17
18
  ## Installation
18
19
 
@@ -34,6 +35,8 @@ Or install it yourself with:
34
35
  gem install can_messenger
35
36
  ```
36
37
 
38
+ RubyGems page: [https://rubygems.org/gems/can_messenger](https://rubygems.org/gems/can_messenger)
39
+
37
40
  ## Usage
38
41
 
39
42
  ### Initializing the Messenger
@@ -127,6 +130,8 @@ messenger.start_listening(dbc: dbc) do |msg|
127
130
  end
128
131
  ```
129
132
 
133
+ If the DBC message definition uses an extended CAN ID, `send_dbc_message` automatically sends it as an extended frame and `start_listening(dbc: ...)` decodes received extended frames back through the same DBC definition.
134
+
130
135
  ### Stopping the Listener
131
136
 
132
137
  To stop listening, use:
@@ -206,7 +211,60 @@ Before using `can_messenger`, please note the following:
206
211
 
207
212
  ## Development
208
213
 
209
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test:rspec` to execute the test suite.
214
+ ### Docker-first workflow
215
+
216
+ Build the development image:
217
+
218
+ ```bash
219
+ docker compose build app
220
+ ```
221
+
222
+ The Ruby services automatically run `bundle check || bundle install` inside the container, so an existing bundle cache volume stays usable after dependency changes.
223
+
224
+ Run RuboCop:
225
+
226
+ ```bash
227
+ docker compose run --rm lint
228
+ ```
229
+
230
+ Run the test suite:
231
+
232
+ ```bash
233
+ docker compose run --rm test
234
+ ```
235
+
236
+ Build the gem:
237
+
238
+ ```bash
239
+ docker compose run --rm build
240
+ ```
241
+
242
+ Install docs dependencies (Docusaurus):
243
+
244
+ ```bash
245
+ docker compose run --rm docs npm ci
246
+ ```
247
+
248
+ Build the docs site:
249
+
250
+ ```bash
251
+ docker compose run --rm docs npm run build
252
+ ```
253
+
254
+ Preview docs locally:
255
+
256
+ ```bash
257
+ docker compose run --rm --service-ports docs npm run start
258
+ ```
259
+
260
+ ### Local Ruby workflow
261
+
262
+ If you already have Ruby installed locally, you can still use:
263
+
264
+ ```bash
265
+ bin/setup
266
+ bundle exec rake test:rspec
267
+ ```
210
268
 
211
269
  ## Contributing
212
270
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "socket"
4
4
  require_relative "base"
5
+ require_relative "../constants"
5
6
 
6
7
  module CanMessenger
7
8
  module Adapter
@@ -11,10 +12,11 @@ module CanMessenger
11
12
  CANFD_FRAME_SIZE = 72
12
13
  MIN_FRAME_SIZE = 8
13
14
  MAX_FD_DATA = 64
15
+ MAX_STANDARD_ID = 0x7FF
14
16
  TIMEOUT = [1, 0].pack("l_2")
15
17
 
16
18
  # Creates and configures a CAN socket bound to the interface.
17
- def open_socket(can_fd: false)
19
+ def open_socket(can_fd: nil)
18
20
  socket = Socket.open(Socket::PF_CAN, Socket::SOCK_RAW, Socket::CAN_RAW)
19
21
  configure_socket(socket, can_fd: can_fd)
20
22
  socket
@@ -25,17 +27,18 @@ module CanMessenger
25
27
  end
26
28
 
27
29
  # 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
30
+ def build_can_frame(id:, data:, extended_id: false, can_fd: nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
29
31
  if can_fd
30
32
  raise ArgumentError, "CAN FD data cannot exceed #{MAX_FD_DATA} bytes" if data.size > MAX_FD_DATA
31
33
  elsif data.size > 8
32
34
  raise ArgumentError, "CAN data cannot exceed 8 bytes"
33
35
  end
34
36
 
35
- # Mask the ID to 29 bits
36
- can_id = id & 0x1FFFFFFF
37
+ validate_can_id!(id, extended_id: extended_id)
38
+
39
+ can_id = id
37
40
  # Set bit 31 for extended frames
38
- can_id |= 0x80000000 if extended_id
41
+ can_id |= Constants::EXTENDED_ID_FLAG if extended_id
39
42
 
40
43
  # Pack the ID based on endianness
41
44
  id_bytes = endianness == :big ? [can_id].pack("L>") : [can_id].pack("V")
@@ -52,7 +55,7 @@ module CanMessenger
52
55
  end
53
56
 
54
57
  # Reads a frame from the socket and parses it into a hash.
55
- def receive_message(socket:, can_fd: false)
58
+ def receive_message(socket:, can_fd: nil)
56
59
  frame_size = can_fd ? CANFD_FRAME_SIZE : FRAME_SIZE
57
60
  frame = socket.recv(frame_size)
58
61
  return nil if frame.nil? || frame.size < MIN_FRAME_SIZE
@@ -72,8 +75,8 @@ module CanMessenger
72
75
  use_fd = can_fd.nil? ? frame.size >= CANFD_FRAME_SIZE : can_fd
73
76
 
74
77
  raw_id = unpack_frame_id(frame: frame)
75
- extended = raw_id.anybits?(0x80000000)
76
- id = raw_id & 0x1FFFFFFF
78
+ extended = raw_id.anybits?(Constants::EXTENDED_ID_FLAG)
79
+ id = raw_id & Constants::MAX_EXTENDED_ID
77
80
 
78
81
  data_length = if use_fd
79
82
  frame[4].ord
@@ -120,6 +123,16 @@ module CanMessenger
120
123
  rescue StandardError
121
124
  # Ignore close errors so we can report the original failure.
122
125
  end
126
+
127
+ def validate_can_id!(id, extended_id:)
128
+ raise ArgumentError, "id must be an Integer" unless id.is_a?(Integer)
129
+ raise ArgumentError, "CAN id cannot be negative" if id.negative?
130
+
131
+ max_id = extended_id ? Constants::MAX_EXTENDED_ID : MAX_STANDARD_ID
132
+ return if id <= max_id
133
+
134
+ raise ArgumentError, "#{extended_id ? "Extended" : "Standard"} CAN id cannot exceed 0x#{max_id.to_s(16).upcase}"
135
+ end
123
136
  end
124
137
  end
125
138
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanMessenger
4
+ module Constants
5
+ CAN_ID_MASK = 0x1FFFFFFF
6
+ EXTENDED_ID_FLAG = 0x80000000
7
+ MAX_EXTENDED_ID = 0x1FFFFFFF
8
+ end
9
+ end
@@ -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
 
@@ -511,5 +510,22 @@ module CanMessenger
511
510
  msb_set = (value >> (length - 1)).allbits?(1)
512
511
  msb_set ? value - (1 << length) : value
513
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
522
+ end
523
+
524
+ nil
525
+ end
526
+
527
+ def max_allowed_bit_for(message_size_bytes)
528
+ (message_size_bytes * 8) - 1
529
+ end
514
530
  end
515
531
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "logger"
4
4
  require_relative "adapter/socketcan"
5
+ require_relative "constants"
5
6
 
6
7
  module CanMessenger
7
8
  # Messenger
@@ -64,8 +65,8 @@ module CanMessenger
64
65
  def send_dbc_message(message_name:, signals:, dbc: @dbc, extended_id: false, can_fd: nil)
65
66
  raise ArgumentError, "dbc is required" if dbc.nil?
66
67
 
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)
68
+ encoded = normalized_dbc_message(dbc.encode_can(message_name, signals), extended_id: extended_id)
69
+ send_can_message(id: encoded[:id], data: encoded[:data], extended_id: encoded[:extended_id], can_fd: can_fd)
69
70
  rescue ArgumentError
70
71
  raise
71
72
  rescue StandardError => e
@@ -134,7 +135,7 @@ module CanMessenger
134
135
  return if filter && !matches_filter?(message_id: message[:id], filter: filter)
135
136
 
136
137
  if dbc
137
- decoded = dbc.decode_can(message[:id], message[:data])
138
+ decoded = dbc.decode_can(dbc_decode_id(message), message[:data])
138
139
  message[:decoded] = decoded if decoded
139
140
  end
140
141
 
@@ -156,5 +157,27 @@ module CanMessenger
156
157
  else true
157
158
  end
158
159
  end
160
+
161
+ def normalize_can_id(id)
162
+ id & Constants::CAN_ID_MASK
163
+ end
164
+
165
+ def dbc_extended_id?(id)
166
+ id.anybits?(Constants::EXTENDED_ID_FLAG)
167
+ end
168
+
169
+ def normalized_dbc_message(encoded, extended_id:)
170
+ {
171
+ id: normalize_can_id(encoded[:id]),
172
+ data: encoded[:data],
173
+ extended_id: extended_id || dbc_extended_id?(encoded[:id])
174
+ }
175
+ end
176
+
177
+ def dbc_decode_id(message)
178
+ return message[:id] unless message[:extended]
179
+
180
+ message[:id] | Constants::EXTENDED_ID_FLAG
181
+ end
159
182
  end
160
183
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CanMessenger
4
- VERSION = "2.0.0"
4
+ VERSION = "2.2.0"
5
5
  end
data/lib/can_messenger.rb CHANGED
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require_relative "can_messenger/version"
5
+ require_relative "can_messenger/constants"
5
6
  require_relative "can_messenger/messenger"
6
7
  require_relative "can_messenger/dbc"
7
8
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: can_messenger
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - fk1018
@@ -23,15 +23,16 @@ files:
23
23
  - lib/can_messenger.rb
24
24
  - lib/can_messenger/adapter/base.rb
25
25
  - lib/can_messenger/adapter/socketcan.rb
26
+ - lib/can_messenger/constants.rb
26
27
  - lib/can_messenger/dbc.rb
27
28
  - lib/can_messenger/messenger.rb
28
29
  - lib/can_messenger/version.rb
29
- homepage: https://github.com/fk1018/can_messenger
30
+ homepage: https://can-messenger.github.io/
30
31
  licenses:
31
32
  - MIT
32
33
  metadata:
33
34
  allowed_push_host: https://rubygems.org
34
- homepage_uri: https://github.com/fk1018/can_messenger
35
+ homepage_uri: https://can-messenger.github.io/
35
36
  source_code_uri: https://github.com/fk1018/can_messenger
36
37
  changelog_uri: https://github.com/fk1018/can_messenger/blob/main/CHANGELOG.md
37
38
  rubygems_mfa_required: 'true'