can_messenger 2.3.1 → 2.3.2

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: 69c5314c6880b4bda9d98417e41373982bc5a8bc82c45edce46df8e0df375d08
4
- data.tar.gz: 31a3e9ca727c04480f9fffb93ea06118115e69473d4814a0903b7ad842dc046a
3
+ metadata.gz: 146ac35cbdc1ef83f90ebc6fb0e41385d2c1787cff145aa8ca346ceb606ed9da
4
+ data.tar.gz: b09ee62d02ddd427688c3fa8e33b39ee633108e504b3675386d3d01f30a02f79
5
5
  SHA512:
6
- metadata.gz: 21dafa39e0e12683372645b6f8ecd97ed8be8ae011f154810305c177a2b4ab0fbf4c2d0f6b04ccf6907e61c39ebbfbb5fe7013b7863fe5e588ad843d7b525b25
7
- data.tar.gz: '08fd4c66a964f99a81e724633265c0809e54582d95a61de95ff8821fc309bf6106b85825fd594313dc9aa32f0b6f3f155dfed87c2ae587c48899bde5065b9f39'
6
+ metadata.gz: 1072360ce734343ffe92e47c47524f19ca9975705b3d12667e95d1d7ddff980e9ffbcfce630e05c10eacfa07b56dbabd4da0f801cb474d0d639e446f38f8c59e
7
+ data.tar.gz: d1c4ed455124f0fcf8a18985ad111daa3fe429fe688dc40584a31be439afeb34c5f6a32fb91740e03eaa2f8dbccc83eae0fe14aba18b80ad425d37971f91e643
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [2.3.2] - 2026-10-30
4
+
5
+ ### Changed
6
+
7
+ - **Breaking:** Require Ruby 4.0.9 or higher and update the Ruby development and CI images.
8
+ - Update the docs toolchain to Node.js 24.22.1 and Docusaurus 3.10.2, refresh locked dependencies, and override vulnerable transitive dependencies with patched versions.
9
+ - Update the locked Ruby JSON dependency to address published security advisories.
10
+ - DBC signals with integral scaling now decode to `Integer` values to preserve precision; fractional scaling continues to return `Float` values.
11
+ - Promote the docs site stable snapshot to `2.3.2`.
12
+
13
+ ### Fixed
14
+
15
+ - Let the SocketCAN listener stop on an idle bus by bounding receive waits.
16
+ - Preserve remote transmission requests as distinct frames and skip DBC decoding for them.
17
+ - Reject CAN payload elements outside the integer byte range instead of silently wrapping them.
18
+ - Preserve large integer DBC values and round correctly when directly constructed signals use integer scaling factors.
19
+ - Reject malformed DBC scaling, invalid byte order, unsupported metadata directives, and unsupported floating-point signal types with line context.
20
+ - Declare Logger as a runtime dependency so minimal Bundler applications can load the gem.
21
+ - Ship the RBS signatures in the gem and load them explicitly during signature validation.
22
+
3
23
  ## [2.3.1] - 2026-04-15
4
24
 
5
25
  ### Changed
@@ -200,7 +220,8 @@
200
220
  ## [0.1.0] - 2024-11-10
201
221
 
202
222
  - Initial release
203
- [Unreleased]: https://github.com/fk1018/can_messenger/compare/v2.3.1...HEAD
223
+ [Unreleased]: https://github.com/fk1018/can_messenger/compare/v2.3.2...HEAD
224
+ [2.3.2]: https://github.com/fk1018/can_messenger/compare/v2.3.1...v2.3.2
204
225
  [2.3.1]: https://github.com/fk1018/can_messenger/compare/v2.3.0...v2.3.1
205
226
  [2.3.0]: https://github.com/fk1018/can_messenger/compare/v2.2.0...v2.3.0
206
227
  [2.2.0]: https://github.com/fk1018/can_messenger/compare/v2.1.0...v2.2.0
data/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  ## Requirements
14
14
 
15
- - Ruby 4.0.1 or higher.
15
+ - Ruby 4.0.9 or higher.
16
16
  - Docker (optional, for containerized development without installing Ruby locally).
17
17
 
18
18
  ## Installation
@@ -57,6 +57,8 @@ To send a message:
57
57
  messenger.send_can_message(id: 0x123, data: [0xDE, 0xAD, 0xBE, 0xEF])
58
58
  ```
59
59
 
60
+ Each payload element must be an integer in `0..255`; invalid bytes raise `ArgumentError`.
61
+
60
62
  > **Note:** Under the hood, the gem now writes CAN frames to a raw socket instead of calling `cansend`. No external dependencies are required beyond raw-socket permissions.
61
63
  > The SocketCAN adapter builds the Linux socket address itself, so it does not depend on Ruby exposing `CAN_RAW` or `pack_sockaddr_can`.
62
64
 
@@ -85,6 +87,8 @@ messenger.start_listening do |msg|
85
87
  end
86
88
  ```
87
89
 
90
+ Remote transmission request (RTR) frames include `remote: true`, the requested byte count in `dlc`, and an empty `data` array. They are yielded without DBC decoding because they contain no signal data.
91
+
88
92
  #### Listening with Filters
89
93
 
90
94
  The `start_listening` method supports filtering incoming messages based on CAN ID:
@@ -138,7 +142,9 @@ end
138
142
  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.
139
143
  If DBC decoding raises because the payload does not match the signal definition, the raw frame is still yielded and includes `decode_error: { class:, message: }`.
140
144
 
141
- Malformed DBC files now fail fast with `ArgumentError` that includes the line number and offending line text. Standard metadata directives such as `VERSION`, `NS_`, `BS_`, `BU_`, `CM_`, `VAL_`, `BA_`, `BA_DEF_`, `BA_DEF_DEF_`, `SIG_VALTYPE_`, and `BO_TX_BU_` are still ignored. Other non-empty directives, including unsupported `BA_*` variants, raise with line context.
145
+ Malformed DBC files fail fast with `ArgumentError` that includes the line number and offending line text. Signal byte order must be `@0` or `@1`, and scaling values must be valid numeric literals. Standard metadata directives such as `VERSION`, `NS_`, `BS_`, `BU_`, `CM_`, `VAL_`, `BA_`, `BA_DEF_`, `BA_DEF_DEF_`, and `BO_TX_BU_` are ignored. `SIG_VALTYPE_` accepts integer type `0`; floating-point types `1` and `2` are unsupported and raise explicitly. Other non-empty directives, including unsupported `BA_*` variants, raise with line context.
146
+
147
+ Identity-scaled integer signals preserve all 64 bits during encoding and decoding. Integral scaling returns `Integer` values; fractional scaling returns `Float` values. Directly constructed signals also round correctly with integer scaling factors.
142
148
 
143
149
  ### Stopping the Listener
144
150
 
@@ -148,6 +154,8 @@ To stop listening, use:
148
154
  messenger.stop_listening
149
155
  ```
150
156
 
157
+ The default SocketCAN adapter checks for shutdown at least once per second while the bus is idle. When listening in another thread, join that thread after stopping to wait for socket cleanup.
158
+
151
159
  ### Adapters
152
160
 
153
161
  `CanMessenger::Messenger` delegates low-level CAN bus operations to an adapter. By default it uses the
@@ -199,7 +207,7 @@ Before using `can_messenger`, please note the following:
199
207
  ```
200
208
 
201
209
  - **Threading & Socket Management:**
202
- - **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.
210
+ - **Blocking Behavior:** `start_listening` runs until stopped. The default SocketCAN adapter bounds each receive wait to one second so shutdown also works on an idle bus. Always call `stop_listening` to gracefully shut down the listener; custom adapters should also bound their receive waits.
203
211
  - **Resource Cleanup:** The socket is automatically closed when the listening loop terminates. Stop the listener to avoid resource leaks.
204
212
 
205
213
  - **Logging:**
@@ -278,7 +286,7 @@ If you already have Ruby installed locally, you can still use:
278
286
 
279
287
  ```bash
280
288
  bin/setup
281
- rbs validate
289
+ rbs -I sig -r logger validate
282
290
  bundle exec rake test:rspec
283
291
  bundle exec rubocop
284
292
  ```
@@ -2,11 +2,19 @@
2
2
 
3
3
  module CanMessenger
4
4
  module Adapter
5
- # Base adapter defines the interface for CAN bus adapters.
6
- # Concrete adapters must implement all of the methods defined here.
5
+ # Defines the transport interface used by Messenger.
6
+ #
7
+ # Provides shared interface, logging, and CAN ID byte-order configuration.
8
+ # Concrete adapters implement socket creation, frame building, receiving,
9
+ # and parsing for their transport.
10
+ #
11
+ # @abstract Subclass and implement the four transport methods below.
7
12
  class Base
8
13
  attr_reader :interface_name, :logger, :endianness
9
14
 
15
+ # Detects the host's byte order by comparing native and explicit integer packing.
16
+ #
17
+ # @return [Symbol] :little or :big
10
18
  def self.native_endianness
11
19
  native = pack_uint(1, "I")
12
20
  return :little if native == pack_uint(1, "V")
@@ -16,11 +24,26 @@ module CanMessenger
16
24
  native.bytes.first == 1 ? :little : :big
17
25
  end
18
26
 
27
+ # Packs an unsigned integer for byte-order detection.
28
+ #
29
+ # @param [Integer] value The integer to pack
30
+ # @param [String] template The Array#pack format specifying width and byte order
31
+ # @return [String] The packed bytes
19
32
  def self.pack_uint(value, template)
20
33
  [value].pack(template)
21
34
  end
22
35
  private_class_method :pack_uint
23
36
 
37
+ # Initializes the shared adapter configuration without opening a socket.
38
+ #
39
+ # Resolves :native to the host's byte order so transport methods only need
40
+ # to handle :little and :big.
41
+ #
42
+ # @param [String] interface_name The CAN interface to use (e.g., "can0")
43
+ # @param [Logger] logger The logger for transport errors and diagnostic messages
44
+ # @param [Symbol, String] endianness CAN ID byte order: native, little, or big
45
+ # @return [void]
46
+ # @raise [ArgumentError] If the byte-order setting is unsupported
24
47
  def initialize(interface_name:, logger:, endianness: :native)
25
48
  @interface_name = interface_name
26
49
  @logger = logger
@@ -28,29 +51,64 @@ module CanMessenger
28
51
  @endianness = normalized_endianness == :native ? self.class.native_endianness : normalized_endianness
29
52
  end
30
53
 
31
- # Open a socket for the underlying interface.
32
- # @return [Object] adapter-specific socket
54
+ # Opens a socket for the underlying interface.
55
+ #
56
+ # Messenger owns the returned socket and closes it after sending or listening.
57
+ #
58
+ # @param [Boolean, nil] can_fd Whether to enable CAN FD frames
59
+ # @return [Object, nil] The adapter-specific socket, or nil if opening fails
60
+ # @raise [NotImplementedError] If a subclass does not implement this method
33
61
  def open_socket(can_fd: false)
34
62
  raise NotImplementedError, "open_socket must be implemented in subclasses"
35
63
  end
36
64
 
37
- # Build a frame ready to be written to the socket.
65
+ # Builds a frame ready to be written to the socket.
66
+ #
67
+ # @param [Integer] id The CAN identifier without frame flags
68
+ # @param [Array<Integer>] data The payload bytes, each in 0..255
69
+ # @param [Boolean] extended_id Whether to use a 29-bit identifier instead of an 11-bit identifier
70
+ # @param [Boolean, nil] can_fd Whether to build a CAN FD frame
71
+ # @return [String] The serialized frame
72
+ # @raise [NotImplementedError] If a subclass does not implement this method
38
73
  def build_can_frame(id:, data:, extended_id: false, can_fd: false)
39
74
  raise NotImplementedError, "build_can_frame must be implemented in subclasses"
40
75
  end
41
76
 
42
- # Receive and parse a frame from the socket.
77
+ # Receives and parses a frame from the socket.
78
+ #
79
+ # Implementations should bound their receive wait and return nil on timeout
80
+ # so Messenger can check whether listening has been stopped.
81
+ #
82
+ # @param [Object] socket The adapter-specific socket to read from
83
+ # @param [Boolean, nil] can_fd Whether CAN FD reception is enabled
84
+ # @return [Hash, nil] A message with :id, :data, and :extended, or nil when no message is available
85
+ # @raise [NotImplementedError] If a subclass does not implement this method
43
86
  def receive_message(socket:, can_fd: false)
44
87
  raise NotImplementedError, "receive_message must be implemented in subclasses"
45
88
  end
46
89
 
47
- # Parse a raw frame string into a message hash.
90
+ # Parses a raw frame string into a message hash.
91
+ #
92
+ # Remote transmission requests should include :remote and :dlc, with empty
93
+ # :data, so Messenger can distinguish them from frames containing signals.
94
+ #
95
+ # @param [String] frame The serialized frame
96
+ # @param [Boolean, nil] can_fd Whether to parse CAN FD, or nil for adapter-specific detection
97
+ # @return [Hash, nil] A message with :id, :data, and :extended, or nil if parsing fails
98
+ # @raise [NotImplementedError] If a subclass does not implement this method
48
99
  def parse_frame(frame:, can_fd: false)
49
100
  raise NotImplementedError, "parse_frame must be implemented in subclasses"
50
101
  end
51
102
 
52
103
  private
53
104
 
105
+ # Converts a supported byte-order setting to a symbol.
106
+ #
107
+ # String settings are case-insensitive and may contain surrounding whitespace.
108
+ #
109
+ # @param [Symbol, String] endianness The requested CAN ID byte order
110
+ # @return [Symbol] :native, :little, or :big
111
+ # @raise [ArgumentError] If the setting does not name a supported byte order
54
112
  def normalize_endianness(endianness)
55
113
  normalized = endianness.is_a?(String) ? endianness.strip.downcase.to_sym : endianness
56
114
  return normalized if %i[native little big].include?(normalized)
@@ -1,29 +1,52 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "socket"
4
+ require "io/wait"
4
5
  require_relative "base"
5
6
  require_relative "../constants"
6
7
 
7
8
  module CanMessenger
8
9
  module Adapter
9
- # Adapter implementation for Linux SocketCAN interfaces.
10
+ # Sends and receives frames through Linux SocketCAN raw sockets.
11
+ #
12
+ # Supports classic CAN and CAN FD, with CAN ID byte order resolved by Base.
13
+ # Frame building and parsing can also be used without opening an interface.
14
+ #
15
+ # @example Building a classic CAN frame
16
+ # adapter = CanMessenger::Adapter::Socketcan.new(interface_name: "can0", logger: Logger.new($stdout))
17
+ # frame = adapter.build_can_frame(id: 0x123, data: [0xDE, 0xAD])
18
+ # frame.bytesize # => 16
10
19
  class Socketcan < Base # rubocop:disable Metrics/ClassLength
20
+ # Linux protocol and socket-option identifiers for raw CAN and CAN FD support.
11
21
  CAN_RAW = 1
12
22
  SOL_CAN_BASE = 100
13
23
  SOL_CAN_RAW = SOL_CAN_BASE + CAN_RAW
14
24
  CAN_RAW_FD_FRAMES = 5
25
+
26
+ # Complete frame sizes include the header and the fixed-size payload area.
15
27
  FRAME_SIZE = 16
16
28
  CANFD_FRAME_SIZE = 72
29
+ # Both frame layouts place payload bytes after an eight-byte header.
17
30
  MIN_FRAME_SIZE = 8
18
31
  MAX_FD_DATA = 64
19
32
  MAX_STANDARD_ID = 0x7FF
33
+
34
+ # sockaddr_can has a native family, alignment padding, interface index, and address union.
20
35
  SOCKADDR_CAN_ADDR_SIZE = 16
21
36
  SOCKADDR_CAN_SIZE = 24
22
37
  SOCKADDR_CAN_PADDING = 2
38
+ # Kernel receive timeout as a native timeval: seconds followed by microseconds.
23
39
  TIMEOUT = [1, 0].pack("l_2")
40
+ # Raw CAN does not use the protocol-specific address union.
24
41
  ZERO_CAN_ADDR = "\x00" * SOCKADDR_CAN_ADDR_SIZE
25
42
 
26
43
  # Creates and configures a CAN socket bound to the interface.
44
+ #
45
+ # Socket creation or configuration errors are logged. Any partially opened
46
+ # socket is closed before returning nil.
47
+ #
48
+ # @param [Boolean, nil] can_fd Whether to enable CAN FD reception and transmission
49
+ # @return [Socket, nil] The bound socket, or nil if it could not be opened
27
50
  def open_socket(can_fd: nil)
28
51
  socket = Socket.open(Socket::PF_CAN, Socket::SOCK_RAW, CAN_RAW)
29
52
  configure_socket(socket, can_fd: can_fd)
@@ -35,7 +58,19 @@ module CanMessenger
35
58
  end
36
59
 
37
60
  # Builds a raw CAN or CAN FD frame for SocketCAN.
61
+ #
62
+ # Sets the extended-frame flag when requested and zero-pads the payload area.
63
+ # The header records the supplied byte count, excluding that padding.
64
+ #
65
+ # @param [Integer] id The CAN identifier without frame flags
66
+ # @param [Array<Integer>] data Payload bytes in 0..255; at most 8 for classic CAN or 64 for CAN FD
67
+ # @param [Boolean] extended_id Whether to use a 29-bit identifier instead of an 11-bit identifier
68
+ # @param [Boolean, nil] can_fd Whether to build a CAN FD frame; nil selects classic CAN
69
+ # @return [String] A 16-byte classic CAN frame or a 72-byte CAN FD frame
70
+ # @raise [ArgumentError] If the identifier, payload type, byte values, or payload length are invalid
38
71
  def build_can_frame(id:, data:, extended_id: false, can_fd: nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
72
+ validate_data!(data)
73
+
39
74
  if can_fd
40
75
  raise ArgumentError, "CAN FD data cannot exceed #{MAX_FD_DATA} bytes" if data.size > MAX_FD_DATA
41
76
  elsif data.size > 8
@@ -63,9 +98,19 @@ module CanMessenger
63
98
  end
64
99
 
65
100
  # Reads a frame from the socket and parses it into a hash.
101
+ #
102
+ # Uses a one-second readiness timeout followed by a nonblocking receive so
103
+ # an idle listener can stop. A timeout or loss of readiness returns nil;
104
+ # other receive errors are logged and also return nil.
105
+ #
106
+ # @param [Socket] socket The bound CAN socket to read from
107
+ # @param [Boolean, nil] can_fd Whether to reserve space for CAN FD; nil selects classic CAN
108
+ # @return [Hash, nil] The parsed message, or nil when no frame can be returned
66
109
  def receive_message(socket:, can_fd: nil)
110
+ return nil unless socket.wait_readable(1)
111
+
67
112
  frame_size = can_fd ? CANFD_FRAME_SIZE : FRAME_SIZE
68
- frame = socket.recv(frame_size)
113
+ frame = socket.recv_nonblock(frame_size)
69
114
  return nil if frame.nil? || frame.size < MIN_FRAME_SIZE
70
115
 
71
116
  parse_frame(frame: frame, can_fd: can_fd)
@@ -77,20 +122,27 @@ module CanMessenger
77
122
  end
78
123
 
79
124
  # Parses a raw CAN frame string into a hash with id, data and extended flag.
80
- def parse_frame(frame:, can_fd: nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
125
+ #
126
+ # The :id excludes frame flags. Remote requests also include :remote => true
127
+ # and the requested byte count in :dlc, with empty :data. If the header is
128
+ # present but the declared payload is incomplete, :data is empty.
129
+ # Parsing errors are logged and return nil.
130
+ #
131
+ # @param [String] frame The raw frame bytes, including the header
132
+ # @param [Boolean, nil] can_fd The frame format, or nil to infer it from the frame size
133
+ # @return [Hash, nil] The message, or nil if the header is missing or parsing fails
134
+ def parse_frame(frame:, can_fd: nil) # rubocop:disable Metrics/MethodLength
81
135
  return nil unless frame && frame.size >= MIN_FRAME_SIZE
82
136
 
83
- use_fd = can_fd.nil? ? frame.size >= CANFD_FRAME_SIZE : can_fd
84
-
85
137
  raw_id = unpack_frame_id(frame: frame)
86
138
  extended = raw_id.anybits?(Constants::EXTENDED_ID_FLAG)
87
139
  id = raw_id & Constants::MAX_EXTENDED_ID
88
140
 
89
- data_length = if use_fd
90
- frame[4].ord
91
- else
92
- frame[4].ord & 0x0F
93
- end
141
+ data_length = frame_data_length(frame, can_fd: can_fd)
142
+
143
+ if raw_id.anybits?(Constants::REMOTE_TRANSMISSION_REQUEST_FLAG)
144
+ return { id: id, data: [], extended: extended, remote: true, dlc: data_length }
145
+ end
94
146
 
95
147
  data = if frame.size >= MIN_FRAME_SIZE + data_length
96
148
  frame[MIN_FRAME_SIZE, data_length].unpack("C*")
@@ -106,6 +158,20 @@ module CanMessenger
106
158
 
107
159
  private
108
160
 
161
+ # Reads the declared payload length using the selected frame layout.
162
+ #
163
+ # @param [String] frame A frame containing at least the complete header
164
+ # @param [Boolean, nil] can_fd Whether to use CAN FD, or nil to infer the format from size
165
+ # @return [Integer] The declared number of payload bytes
166
+ def frame_data_length(frame, can_fd:)
167
+ use_fd = can_fd.nil? ? frame.size >= CANFD_FRAME_SIZE : can_fd
168
+ use_fd ? frame[4].ord : frame[4].ord & 0x0F
169
+ end
170
+
171
+ # Reads the CAN identifier and frame flags using the configured byte order.
172
+ #
173
+ # @param [String] frame The raw frame containing the four-byte identifier field
174
+ # @return [Integer] The identifier with any frame flags still set
109
175
  def unpack_frame_id(frame:)
110
176
  if endianness == :big
111
177
  frame[0..3].unpack1("L>")
@@ -114,6 +180,16 @@ module CanMessenger
114
180
  end
115
181
  end
116
182
 
183
+ # Binds a socket, sets its kernel receive timeout, and optionally enables CAN FD.
184
+ #
185
+ # receive_message also bounds its readiness wait because the kernel timeout
186
+ # alone does not limit Ruby's wait for a readable socket.
187
+ #
188
+ # @param [Socket] socket The newly opened CAN socket
189
+ # @param [Boolean, nil] can_fd Whether to enable CAN FD frames
190
+ # @return [void]
191
+ # @raise [ArgumentError] If the configured interface cannot be resolved
192
+ # @raise [SystemCallError] If binding or setting a socket option fails
117
193
  def configure_socket(socket, can_fd:)
118
194
  socket.bind(build_sockaddr_can(interface_index))
119
195
  socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, TIMEOUT)
@@ -122,6 +198,10 @@ module CanMessenger
122
198
  socket.setsockopt(SOL_CAN_RAW, CAN_RAW_FD_FRAMES, 1)
123
199
  end
124
200
 
201
+ # Closes a socket during failed setup without masking the original error.
202
+ #
203
+ # @param [Socket, nil] socket The socket to close, if one was created
204
+ # @return [void]
125
205
  def close_socket(socket)
126
206
  return unless socket
127
207
  return if socket.respond_to?(:closed?) && socket.closed?
@@ -131,6 +211,12 @@ module CanMessenger
131
211
  # Ignore close errors so we can report the original failure.
132
212
  end
133
213
 
214
+ # Resolves the configured interface name to its Linux interface index.
215
+ #
216
+ # Uses Ruby's interface list first, then falls back to the sysfs entry.
217
+ #
218
+ # @return [Integer] The positive interface index used for binding
219
+ # @raise [ArgumentError] If neither lookup can resolve the interface
134
220
  def interface_index
135
221
  index = interface_index_from_ifaddrs || interface_index_from_sysfs
136
222
  return index if index
@@ -138,6 +224,9 @@ module CanMessenger
138
224
  raise ArgumentError, "Unknown CAN interface #{interface_name}"
139
225
  end
140
226
 
227
+ # Looks up the interface index through Ruby's network-interface enumeration.
228
+ #
229
+ # @return [Integer, nil] The index, or nil if unavailable, unsupported, or not found
141
230
  def interface_index_from_ifaddrs
142
231
  return nil unless Socket.respond_to?(:getifaddrs)
143
232
 
@@ -153,6 +242,9 @@ module CanMessenger
153
242
  nil
154
243
  end
155
244
 
245
+ # Reads the Linux sysfs interface index as a fallback for Ruby's interface lookup.
246
+ #
247
+ # @return [Integer, nil] The positive index, or nil if the entry is missing, unreadable, or invalid
156
248
  def interface_index_from_sysfs
157
249
  ifindex_path = File.join("/sys/class/net", interface_name, "ifindex")
158
250
  return nil unless File.file?(ifindex_path)
@@ -163,6 +255,14 @@ module CanMessenger
163
255
  nil
164
256
  end
165
257
 
258
+ # Packs the native Linux sockaddr_can structure used to bind a raw socket.
259
+ #
260
+ # Socket addresses always use host byte order, independently of the byte
261
+ # order selected for CAN identifiers in frame building and parsing.
262
+ #
263
+ # @param [Integer] ifindex The resolved Linux interface index
264
+ # @return [String] The 24-byte packed socket address
265
+ # @raise [RuntimeError] If the packed layout does not have the expected size
166
266
  def build_sockaddr_can(ifindex)
167
267
  can_family = Socket.const_defined?(:AF_CAN) ? Socket::AF_CAN : Socket::PF_CAN
168
268
  sockaddr = [can_family, ifindex, ZERO_CAN_ADDR].pack("S!x#{SOCKADDR_CAN_PADDING}i!a#{SOCKADDR_CAN_ADDR_SIZE}")
@@ -171,6 +271,12 @@ module CanMessenger
171
271
  raise "sockaddr_can must be #{SOCKADDR_CAN_SIZE} bytes, got #{sockaddr.bytesize}"
172
272
  end
173
273
 
274
+ # Checks that an identifier fits the selected CAN frame format without flags.
275
+ #
276
+ # @param [Object] id The caller-supplied identifier to validate
277
+ # @param [Boolean] extended_id Whether the allowed range is 29 bits instead of 11 bits
278
+ # @return [void]
279
+ # @raise [ArgumentError] If the identifier is not an integer or lies outside the allowed range
174
280
  def validate_can_id!(id, extended_id:)
175
281
  raise ArgumentError, "id must be an Integer" unless id.is_a?(Integer)
176
282
  raise ArgumentError, "CAN id cannot be negative" if id.negative?
@@ -180,6 +286,21 @@ module CanMessenger
180
286
 
181
287
  raise ArgumentError, "#{extended_id ? "Extended" : "Standard"} CAN id cannot exceed 0x#{max_id.to_s(16).upcase}"
182
288
  end
289
+
290
+ # Checks the payload container and each byte before binary packing.
291
+ #
292
+ # Explicit validation prevents Array#pack from silently wrapping integers
293
+ # outside the byte range or coercing noninteger values.
294
+ #
295
+ # @param [Object] data The caller-supplied payload to validate
296
+ # @return [void]
297
+ # @raise [ArgumentError] If the payload is not an array of integers in 0..255
298
+ def validate_data!(data)
299
+ raise ArgumentError, "data must be an Array of byte Integers" unless data.is_a?(Array)
300
+ return if data.all? { |byte| byte.is_a?(Integer) && byte.between?(0, 255) }
301
+
302
+ raise ArgumentError, "data bytes must be Integers between 0 and 255"
303
+ end
183
304
  end
184
305
  end
185
306
  end
@@ -1,9 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CanMessenger
4
+ # Shared CAN identifier masks and frame flags.
5
+ #
6
+ # SocketCAN stores frame flags above the 29 identifier bits. DBC message IDs
7
+ # also use bit 31 to mark extended frames, so the same flag is used when
8
+ # translating between DBC definitions and received frames.
4
9
  module Constants
10
+ # Keeps only the 29 identifier bits and removes the frame flags.
5
11
  CAN_ID_MASK = 0x1FFFFFFF
12
+ # Bit 31 selects the extended frame format with a 29-bit identifier.
6
13
  EXTENDED_ID_FLAG = 0x80000000
14
+ # Bit 30 marks a remote request for data, which carries no signal payload.
15
+ REMOTE_TRANSMISSION_REQUEST_FLAG = 0x40000000
16
+ # Largest valid extended identifier before any frame flags are added.
7
17
  MAX_EXTENDED_ID = 0x1FFFFFFF
8
18
  end
9
19
  end
@@ -6,6 +6,8 @@ module CanMessenger
6
6
  # This class provides functionality to parse DBC files and encode/decode CAN messages
7
7
  # according to the signal definitions. DBC files are a standard way to describe
8
8
  # CAN network communication.
9
+ # Integer signals support signed or unsigned values, both byte orders, and scaling.
10
+ # Unsupported floating-point signal declarations raise instead of being decoded as integers.
9
11
  #
10
12
  # @example Loading and using a DBC file
11
13
  # dbc = CanMessenger::DBC.load('vehicle.dbc')
@@ -17,7 +19,8 @@ module CanMessenger
17
19
  # # Decode a received CAN frame
18
20
  # decoded = dbc.decode_can(0x123, [0x09, 0xC4, 0xAB, 0x00, 0x00, 0x00, 0x00, 0x00])
19
21
  # # => { name: 'EngineData', signals: { RPM: 2500.0, Temperature: 85.5 } }
20
- class DBC
22
+ class DBC # rubocop:disable Metrics/ClassLength
23
+ # Metadata directive names that can be skipped; matching uses complete tokens.
21
24
  IGNORED_LINE_PREFIXES = %w[
22
25
  BO_TX_BU_
23
26
  VERSION
@@ -29,12 +32,19 @@ module CanMessenger
29
32
  BA_
30
33
  BA_DEF_
31
34
  BA_DEF_DEF_
32
- SIG_VALTYPE_
33
35
  ].freeze
34
36
 
37
+ # Numeric tokens allow a sign, decimal point, and optional scientific exponent.
38
+ NUMBER_PATTERN = /[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/
39
+ # Message headers provide the stored identifier, name, payload size, and sender.
35
40
  MESSAGE_LINE_PATTERN = /^BO_\s+(\d+)\s+(\w+)\s*:\s*(\d+)\s+\S.*$/
36
- SIGNAL_LINE_PATTERN = /^SG_\s+(\w+)\s*:\s*(\d+)\|(\d+)@(\d)([+-])\s*\(([^,]+),([^)]+)\)/
41
+ # Capture the signal layout and scaling; byte order is restricted to @0 or @1.
42
+ SIGNAL_LINE_PATTERN = /^SG_\s+(\w+)\s*:\s*(\d+)\|(\d+)@([01])([+-])\s*
43
+ \(\s*(#{NUMBER_PATTERN})\s*,\s*(#{NUMBER_PATTERN})\s*\)/x
44
+ # Type 0 is an integer; types 1 and 2 declare unsupported IEEE float and double signals.
45
+ SIGNAL_VALUE_TYPE_PATTERN = /\ASIG_VALTYPE_\s+\d+\s+\w+\s*:\s*(\d+)\s*;\z/
37
46
 
47
+ # @return [Hash<String, Message>] Parsed message definitions keyed by their names
38
48
  attr_reader :messages
39
49
 
40
50
  # Loads a DBC file from disk and parses its contents.
@@ -50,6 +60,8 @@ module CanMessenger
50
60
  # Initializes a new DBC instance.
51
61
  #
52
62
  # @param [String] content The DBC file content to parse (optional)
63
+ # @return [void]
64
+ # @raise [ArgumentError] If the content contains a malformed or unsupported definition
53
65
  def initialize(content = "")
54
66
  @messages = {}
55
67
  parse(content) unless content.empty?
@@ -60,9 +72,11 @@ module CanMessenger
60
72
  # This method processes each line of the DBC content, identifying message
61
73
  # definitions (BO_) and signal definitions (SG_). It builds a complete
62
74
  # message structure with all associated signals.
75
+ # Existing messages are retained unless a new definition uses the same name.
63
76
  #
64
77
  # @param [String] content The DBC file content to parse
65
78
  # @return [void]
79
+ # @raise [ArgumentError] If a definition is malformed or unsupported, with line number and text
66
80
  def parse(content) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
67
81
  current = nil
68
82
  in_namespace_section = false
@@ -70,11 +84,10 @@ module CanMessenger
70
84
  # The parser is intentionally strict so malformed DBC files fail early with line context.
71
85
  # We still ignore a small allowlist of standard metadata directives that do not affect encode/decode.
72
86
  content.each_line.with_index(1) do |line, line_number|
73
- raw_line = line.rstrip
74
- stripped_line = raw_line.strip
87
+ stripped_line = line.strip
75
88
 
76
89
  if in_namespace_section
77
- next if raw_line.match?(/^\s+/)
90
+ next if line.match?(/^\s+\S/)
78
91
 
79
92
  in_namespace_section = false
80
93
  end
@@ -99,6 +112,8 @@ module CanMessenger
99
112
  raise_parse_error("Invalid signal definition", line_number, stripped_line) unless sig
100
113
 
101
114
  current.signals << sig
115
+ elsif stripped_line.start_with?("SIG_VALTYPE_")
116
+ validate_signal_value_type(stripped_line, line_number)
102
117
  else
103
118
  raise_parse_error("Unsupported or malformed DBC line", line_number, stripped_line)
104
119
  end
@@ -127,24 +142,21 @@ module CanMessenger
127
142
  #
128
143
  # @param [String] line A single line from the DBC file
129
144
  # @param [Message] _current The current message being processed (unused but kept for API consistency)
130
- # @return [Signal, nil] A Signal object if the line matches, nil otherwise
145
+ # @return [Signal, nil] The signal, or nil if its layout or scaling cannot be parsed
131
146
  def parse_signal_line(line, _current) # rubocop:disable Metrics/MethodLength
132
147
  return unless (m = line.match(SIGNAL_LINE_PATTERN))
133
148
 
134
- sig_name = m[1]
135
- start_bit = m[2].to_i
136
- length = m[3].to_i
137
- endian = m[4] == "1" ? :little : :big
138
- sign = m[5] == "-" ? :signed : :unsigned
139
- factor = m[6].to_f
140
- offset = m[7].to_f
149
+ factor = parse_number(m[6])
150
+ offset = parse_number(m[7])
151
+ return unless factor && offset
152
+ return if factor.zero?
141
153
 
142
154
  Signal.new(
143
- sig_name,
144
- start_bit: start_bit,
145
- length: length,
146
- endianness: endian,
147
- sign: sign,
155
+ m[1],
156
+ start_bit: m[2].to_i,
157
+ length: m[3].to_i,
158
+ endianness: m[4] == "1" ? :little : :big,
159
+ sign: m[5] == "-" ? :signed : :unsigned,
148
160
  factor: factor,
149
161
  offset: offset
150
162
  )
@@ -158,7 +170,7 @@ module CanMessenger
158
170
  # @param [String] name The name of the message to encode
159
171
  # @param [Hash<Symbol|String, Numeric>] values Signal names mapped to their values
160
172
  # @return [Hash] A hash containing :id (Integer) and :data (Array<Integer>)
161
- # @raise [ArgumentError] If the message name is not found in the DBC
173
+ # @raise [ArgumentError] If the name is unknown, a value is out of range, or a signal exceeds the payload
162
174
  #
163
175
  # @example
164
176
  # frame = dbc.encode_can('EngineData', RPM: 2500, Temperature: 85.5)
@@ -174,10 +186,13 @@ module CanMessenger
174
186
  #
175
187
  # Takes a CAN ID and data bytes, finds the matching message definition,
176
188
  # and decodes the data into individual signal values according to the DBC.
189
+ # The identifier must match the stored DBC ID, including its extended-frame
190
+ # flag when present; Messenger handles this conversion for received frames.
177
191
  #
178
192
  # @param [Integer] id The CAN message ID
179
193
  # @param [Array<Integer>] data The CAN message data bytes
180
194
  # @return [Hash, nil] A hash containing :name (String) and :signals (Hash), or nil if no matching message
195
+ # @raise [ArgumentError] If a signal references bits outside the supplied payload
181
196
  #
182
197
  # @example
183
198
  # decoded = dbc.decode_can(0x123, [0x09, 0xC4, 0xAB, 0x00, 0x00, 0x00, 0x00, 0x00])
@@ -191,10 +206,58 @@ module CanMessenger
191
206
 
192
207
  private
193
208
 
209
+ # Checks whether a line begins with an explicitly supported metadata directive.
210
+ #
211
+ # Comparing the complete token prevents unsupported names such as BA_UNKNOWN_
212
+ # from being mistaken for the supported BA_ directive.
213
+ #
214
+ # @param [String] line The stripped DBC line
215
+ # @return [Boolean] Whether the parser can skip this directive
194
216
  def ignored_line?(line)
195
- line.start_with?(*IGNORED_LINE_PREFIXES)
217
+ IGNORED_LINE_PREFIXES.include?(line.split(/[\s:]/, 2).first)
218
+ end
219
+
220
+ # Parses a scaling token without losing precision for integral values.
221
+ #
222
+ # Decimal and exponent spellings such as "1.0" and "1e0" remain exact integers.
223
+ # Fractional values use Float to retain the existing scaling behavior.
224
+ #
225
+ # @param [String] value A numeric token matched by the signal pattern
226
+ # @return [Integer, Float, nil] The parsed value, or nil if a fractional value is not finite
227
+ def parse_number(value)
228
+ value = value.sub(/\.(?=[eE]|\z)/, ".0")
229
+ exact = Rational(value)
230
+ return exact.to_i if exact.denominator == 1
231
+
232
+ number = Float(value)
233
+ number if number.finite?
234
+ end
235
+
236
+ # Validates a SIG_VALTYPE_ declaration and rejects unsupported signal representations.
237
+ #
238
+ # Only integer type 0 is supported. Accepting IEEE types 1 or 2 without handling
239
+ # their binary representation would silently change the encoded or decoded value.
240
+ #
241
+ # @param [String] line The stripped signal-type declaration
242
+ # @param [Integer] line_number The one-based line number in the DBC content
243
+ # @return [void]
244
+ # @raise [ArgumentError] If the declaration is malformed or names an unsupported type
245
+ def validate_signal_value_type(line, line_number)
246
+ match = line.match(SIGNAL_VALUE_TYPE_PATTERN)
247
+ raise_parse_error("Invalid signal value type definition", line_number, line) unless match
248
+ return if match[1] == "0"
249
+
250
+ raise_parse_error("Unsupported signal value type #{match[1]} (only integer type 0 is supported)",
251
+ line_number, line)
196
252
  end
197
253
 
254
+ # Raises a parse error with enough source context to locate the definition.
255
+ #
256
+ # @param [String] reason The explanation of the parse failure
257
+ # @param [Integer] line_number The one-based line number in the DBC content
258
+ # @param [String] line The offending DBC line
259
+ # @return [void]
260
+ # @raise [ArgumentError] Always, with the reason, line number, and offending text
198
261
  def raise_parse_error(reason, line_number, line)
199
262
  raise ArgumentError, "#{reason} at line #{line_number}: #{line}"
200
263
  end
@@ -214,9 +277,10 @@ module CanMessenger
214
277
 
215
278
  # Initializes a new Message instance.
216
279
  #
217
- # @param [Integer] id The CAN message ID (11-bit standard or 29-bit extended)
280
+ # @param [Integer] id The stored CAN ID, including the DBC extended-frame flag when present
218
281
  # @param [String] name The symbolic name of the message
219
- # @param [Integer] dlc Data Length Code - number of bytes in the message (0-8 for classic CAN)
282
+ # @param [Integer] dlc Payload size in bytes; at most 8 for classic CAN or 64 for CAN FD
283
+ # @return [void]
220
284
  def initialize(id, name, dlc)
221
285
  @id = id
222
286
  @name = name
@@ -228,9 +292,11 @@ module CanMessenger
228
292
  #
229
293
  # Iterates through all signals in the message and encodes their values
230
294
  # into the appropriate bit positions within the message data bytes.
295
+ # The new payload is filled with zeros, and only supplied signal values are written.
231
296
  #
232
297
  # @param [Hash<Symbol|String, Numeric>] values Signal names mapped to their values
233
298
  # @return [Array<Integer>] Array of bytes representing the encoded message
299
+ # @raise [ArgumentError] If a value is out of range or a signal exceeds the payload size
234
300
  def encode(values)
235
301
  bytes = Array.new(@dlc, 0)
236
302
  @signals.each do |sig|
@@ -249,7 +315,8 @@ module CanMessenger
249
315
  # the final engineering unit values.
250
316
  #
251
317
  # @param [Array<Integer>] data The message data bytes to decode
252
- # @return [Hash<Symbol, Float>] Signal names mapped to their decoded values
318
+ # @return [Hash<Symbol, Numeric>] Signal names mapped to their decoded values
319
+ # @raise [ArgumentError] If a signal references bits outside the supplied payload
253
320
  def decode(data)
254
321
  res = {}
255
322
  @signals.each do |sig|
@@ -285,16 +352,17 @@ module CanMessenger
285
352
  # @param [Integer] length The number of bits the signal occupies (1-64)
286
353
  # @param [Symbol] endianness Byte order - :little for little-endian, :big for big-endian
287
354
  # @param [Symbol] sign Value type - :unsigned for unsigned integers, :signed for signed integers
288
- # @param [Float] factor Scaling factor to convert raw value to engineering units
289
- # @param [Float] offset Offset to add after scaling
355
+ # @param [Numeric] factor Scaling factor to convert raw value to engineering units
356
+ # @param [Numeric] offset Offset to add after scaling
357
+ # @return [void]
290
358
  def initialize(name, start_bit:, length:, endianness:, sign:, factor:, offset:) # rubocop:disable Metrics/ParameterLists
291
359
  @name = name
292
360
  @start_bit = start_bit
293
361
  @length = length
294
362
  @endianness = endianness
295
363
  @sign = sign
296
- @factor = factor
297
- @offset = offset
364
+ @factor = normalize_integral_number(factor)
365
+ @offset = normalize_integral_number(offset)
298
366
  end
299
367
 
300
368
  # Encodes a value into the message byte array at this signal's bit position.
@@ -302,13 +370,14 @@ module CanMessenger
302
370
  # Converts the engineering unit value to a raw integer using the signal's
303
371
  # factor and offset, then places the bits into the appropriate positions
304
372
  # within the message bytes.
373
+ # Fractional raw values round to the nearest integer, with ties away from zero.
305
374
  #
306
375
  # @param [Array<Integer>] bytes The message byte array to modify
307
376
  # @param [Numeric] value The engineering unit value to encode
308
377
  # @return [void]
309
378
  # @raise [ArgumentError] If the value is out of range or signal exceeds message bounds
310
379
  def encode(bytes, value)
311
- raw = ((value - offset) / factor).round
380
+ raw = (value - offset).quo(factor).round
312
381
  validate_signal_bounds(bytes.size)
313
382
  insert_bits(bytes, raw)
314
383
  end
@@ -318,9 +387,11 @@ module CanMessenger
318
387
  # Extracts the raw integer value from the appropriate bit positions,
319
388
  # then applies the signal's scaling (factor and offset) to convert
320
389
  # it to engineering units.
390
+ # Integer factors and offsets preserve exact Integer results; fractional Float scaling returns a Float.
321
391
  #
322
392
  # @param [Array<Integer>] bytes The message byte array to decode from
323
- # @return [Float] The decoded value in engineering units
393
+ # @return [Numeric] The decoded value in engineering units
394
+ # @raise [ArgumentError] If a signal bit falls outside the supplied payload
324
395
  def decode(bytes)
325
396
  raw = extract_bits(bytes)
326
397
  (raw * factor) + offset
@@ -328,6 +399,19 @@ module CanMessenger
328
399
 
329
400
  private
330
401
 
402
+ # Converts integral Float scaling values to Integer without changing fractional values.
403
+ #
404
+ # Keeping identity scaling such as 1.0 and 0.0 integral avoids rounding large
405
+ # signal values through floating-point arithmetic.
406
+ #
407
+ # @param [Numeric] number The factor or offset supplied to the signal constructor
408
+ # @return [Numeric] An Integer for integral finite Floats, otherwise the original number
409
+ def normalize_integral_number(number)
410
+ return number unless number.is_a?(Float) && number.finite? && number.denominator == 1
411
+
412
+ number.to_i
413
+ end
414
+
331
415
  # Validates that the signal fits within the message boundaries.
332
416
  #
333
417
  # Ensures that all bits used by this signal fall within the message's
@@ -565,10 +649,25 @@ module CanMessenger
565
649
  msb_set ? value - (1 << length) : value
566
650
  end
567
651
 
652
+ # Reverses the bit numbering within each byte for Motorola signal positions.
653
+ #
654
+ # DBC sawtooth numbering labels a byte's least significant bit as 0, whereas
655
+ # sequential network numbering starts with its most significant bit. Applying
656
+ # this conversion twice returns the original position.
657
+ #
658
+ # @param [Integer] bitnum The absolute bit position in either numbering scheme
659
+ # @return [Integer] The corresponding position in the other scheme
568
660
  def sawtooth_to_network_bitnum(bitnum)
569
661
  (8 * (bitnum / 8)) + (7 - (bitnum % 8))
570
662
  end
571
663
 
664
+ # Finds the first signal bit that falls outside the payload boundaries.
665
+ #
666
+ # Checks mapped positions so Motorola signals spanning multiple bytes use
667
+ # the same bounds rules as their encoding and decoding paths.
668
+ #
669
+ # @param [Integer] max_allowed_bit The highest valid bit index in the payload
670
+ # @return [Integer, nil] The first invalid position encountered, or nil if all positions fit
572
671
  def invalid_bit_position(max_allowed_bit)
573
672
  length.times do |i|
574
673
  bit_pos = calculate_bit_position(i)
@@ -578,6 +677,10 @@ module CanMessenger
578
677
  nil
579
678
  end
580
679
 
680
+ # Converts a payload size to its highest valid zero-based bit index.
681
+ #
682
+ # @param [Integer] message_size_bytes The payload size in bytes
683
+ # @return [Integer] The highest bit index, or -1 for an empty payload
581
684
  def max_allowed_bit_for(message_size_bytes)
582
685
  (message_size_bytes * 8) - 1
583
686
  end
@@ -132,6 +132,7 @@ module CanMessenger
132
132
  # @return [void]
133
133
  def process_message(socket, filter, can_fd, dbc, &block)
134
134
  message = @adapter.receive_message(socket: socket, can_fd: can_fd)
135
+ return unless @listening
135
136
  return if message.nil?
136
137
  return if filter && !matches_filter?(message_id: message[:id], filter: filter)
137
138
 
@@ -186,6 +187,8 @@ module CanMessenger
186
187
  end
187
188
 
188
189
  def attach_dbc_payload!(message, dbc)
190
+ return if message[:remote]
191
+
189
192
  decoded = dbc.decode_can(dbc_decode_id(message), message[:data])
190
193
  message[:decoded] = decoded if decoded
191
194
  rescue StandardError => e
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CanMessenger
4
- VERSION = "2.3.1"
4
+ # Gem release version used by packaging and available to applications at runtime.
5
+ VERSION = "2.3.2"
5
6
  end
data/lib/can_messenger.rb CHANGED
@@ -6,6 +6,9 @@ require_relative "can_messenger/constants"
6
6
  require_relative "can_messenger/messenger"
7
7
  require_relative "can_messenger/dbc"
8
8
 
9
+ # Public namespace for CAN messaging, transport adapters, and DBC encoding/decoding.
10
+ # Requiring "can_messenger" loads these components together.
9
11
  module CanMessenger
12
+ # Library-specific error type available to callers and adapter extensions.
10
13
  class Error < StandardError; end
11
14
  end
@@ -0,0 +1,42 @@
1
+ module CanMessenger
2
+ module Adapter
3
+ class Base
4
+ @interface_name: String
5
+ @logger: Logger
6
+ @endianness: Symbol
7
+
8
+ attr_reader interface_name: String
9
+ attr_reader logger: Logger
10
+ attr_reader endianness: Symbol
11
+
12
+ def self.native_endianness: () -> Symbol
13
+
14
+ def initialize: (
15
+ interface_name: String,
16
+ logger: Logger,
17
+ ?endianness: (String | Symbol)
18
+ ) -> void
19
+
20
+ def open_socket: (?can_fd: bool?) -> untyped
21
+ def build_can_frame: (
22
+ id: Integer,
23
+ data: Array[Integer],
24
+ ?extended_id: bool,
25
+ ?can_fd: bool?
26
+ ) -> String
27
+ def receive_message: (
28
+ socket: untyped,
29
+ ?can_fd: bool?
30
+ ) -> ({ id: Integer, data: Array[Integer], extended: bool, ?remote: bool, ?dlc: Integer } | nil)
31
+ def parse_frame: (
32
+ frame: String,
33
+ ?can_fd: bool?
34
+ ) -> ({ id: Integer, data: Array[Integer], extended: bool, ?remote: bool, ?dlc: Integer } | nil)
35
+
36
+ private
37
+
38
+ def self.pack_uint: (Integer value, String template) -> String
39
+ def normalize_endianness: ((String | Symbol) endianness) -> Symbol
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,49 @@
1
+ module CanMessenger
2
+ module Adapter
3
+ class Socketcan < Base
4
+ CAN_RAW: Integer
5
+ SOL_CAN_BASE: Integer
6
+ SOL_CAN_RAW: Integer
7
+ CAN_RAW_FD_FRAMES: Integer
8
+ FRAME_SIZE: Integer
9
+ CANFD_FRAME_SIZE: Integer
10
+ MIN_FRAME_SIZE: Integer
11
+ MAX_FD_DATA: Integer
12
+ MAX_STANDARD_ID: Integer
13
+ SOCKADDR_CAN_ADDR_SIZE: Integer
14
+ SOCKADDR_CAN_SIZE: Integer
15
+ SOCKADDR_CAN_PADDING: Integer
16
+ TIMEOUT: String
17
+ ZERO_CAN_ADDR: String
18
+
19
+ def open_socket: (?can_fd: bool?) -> untyped
20
+ def build_can_frame: (
21
+ id: Integer,
22
+ data: Array[Integer],
23
+ ?extended_id: bool,
24
+ ?can_fd: bool?
25
+ ) -> String
26
+ def receive_message: (
27
+ socket: untyped,
28
+ ?can_fd: bool?
29
+ ) -> ({ id: Integer, data: Array[Integer], extended: bool, ?remote: bool, ?dlc: Integer } | nil)
30
+ def parse_frame: (
31
+ frame: String,
32
+ ?can_fd: bool?
33
+ ) -> ({ id: Integer, data: Array[Integer], extended: bool, ?remote: bool, ?dlc: Integer } | nil)
34
+
35
+ private
36
+
37
+ def frame_data_length: (String frame, can_fd: bool?) -> Integer
38
+ def unpack_frame_id: (frame: String) -> Integer
39
+ def configure_socket: (untyped socket, can_fd: bool?) -> void
40
+ def close_socket: (untyped socket) -> void
41
+ def interface_index: () -> Integer
42
+ def interface_index_from_ifaddrs: () -> Integer?
43
+ def interface_index_from_sysfs: () -> Integer?
44
+ def build_sockaddr_can: (Integer ifindex) -> String
45
+ def validate_can_id!: (untyped id, extended_id: bool) -> void
46
+ def validate_data!: (untyped data) -> void
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,8 @@
1
+ module CanMessenger
2
+ module Constants
3
+ CAN_ID_MASK: Integer
4
+ EXTENDED_ID_FLAG: Integer
5
+ REMOTE_TRANSMISSION_REQUEST_FLAG: Integer
6
+ MAX_EXTENDED_ID: Integer
7
+ end
8
+ end
@@ -0,0 +1,158 @@
1
+ module CanMessenger
2
+ # DBC (Database CAN) Parser and Encoder/Decoder
3
+ class DBC
4
+ IGNORED_LINE_PREFIXES: Array[String]
5
+ NUMBER_PATTERN: Regexp
6
+ MESSAGE_LINE_PATTERN: Regexp
7
+ SIGNAL_LINE_PATTERN: Regexp
8
+ SIGNAL_VALUE_TYPE_PATTERN: Regexp
9
+
10
+ @messages: Hash[String, Message]
11
+
12
+ attr_reader messages: Hash[String, Message]
13
+
14
+ # Loads a DBC file from disk and parses its contents
15
+ def self.load: (String path) -> DBC
16
+
17
+ # Initializes a new DBC instance
18
+ def initialize: (?String content) -> void
19
+
20
+ # Parses DBC content and populates the messages hash
21
+ def parse: (String content) -> void
22
+
23
+ # Parses a message definition line from DBC content
24
+ def parse_message_line: (String line) -> (Message | nil)
25
+
26
+ # Parses a signal definition line from DBC content
27
+ def parse_signal_line: (String line, Message? _current) -> (Signal | nil)
28
+
29
+ # Encodes signal values into a CAN message frame
30
+ def encode_can: (String name, Hash[Symbol | String, Numeric] values) -> { id: Integer, data: Array[Integer] }
31
+
32
+ # Decodes a CAN message frame into signal values
33
+ def decode_can: (Integer id, Array[Integer] data) -> ({ name: String, signals: Hash[Symbol, Numeric] } | nil)
34
+
35
+ private
36
+
37
+ def ignored_line?: (String line) -> bool
38
+ def parse_number: (String value) -> (Integer | Float | nil)
39
+ def validate_signal_value_type: (String line, Integer line_number) -> void
40
+ def raise_parse_error: (String reason, Integer line_number, String line) -> void
41
+ end
42
+
43
+ # Represents a CAN message definition from a DBC file
44
+ class Message
45
+ @id: Integer
46
+ @name: String
47
+ @dlc: Integer
48
+ @signals: Array[Signal]
49
+
50
+ attr_reader id: Integer
51
+ attr_reader name: String
52
+ attr_reader dlc: Integer
53
+ attr_reader signals: Array[Signal]
54
+
55
+ # Initializes a new Message instance
56
+ def initialize: (Integer id, String name, Integer dlc) -> void
57
+
58
+ # Encodes signal values into the message byte array
59
+ def encode: (Hash[Symbol | String, Numeric] values) -> Array[Integer]
60
+
61
+ # Decodes message data bytes into individual signal values
62
+ def decode: (Array[Integer] data) -> Hash[Symbol, Numeric]
63
+ end
64
+
65
+ # Represents a signal within a CAN message
66
+ class Signal
67
+ @name: String
68
+ @start_bit: Integer
69
+ @length: Integer
70
+ @endianness: Symbol
71
+ @sign: Symbol
72
+ @factor: Numeric
73
+ @offset: Numeric
74
+
75
+ attr_reader name: String
76
+ attr_reader start_bit: Integer
77
+ attr_reader length: Integer
78
+ attr_reader endianness: Symbol
79
+ attr_reader sign: Symbol
80
+ attr_reader factor: Numeric
81
+ attr_reader offset: Numeric
82
+
83
+ # Initializes a new Signal instance
84
+ def initialize: (
85
+ String name,
86
+ start_bit: Integer,
87
+ length: Integer,
88
+ endianness: Symbol,
89
+ sign: Symbol,
90
+ factor: Numeric,
91
+ offset: Numeric
92
+ ) -> void
93
+
94
+ # Encodes a value into the message byte array at this signal's bit position
95
+ def encode: (Array[Integer] bytes, Numeric value) -> void
96
+
97
+ # Decodes this signal's value from the message byte array
98
+ def decode: (Array[Integer] bytes) -> Numeric
99
+
100
+ private
101
+
102
+ def normalize_integral_number: (Numeric number) -> Numeric
103
+
104
+ # Validates that the signal fits within the message boundaries
105
+ def validate_signal_bounds: (Integer message_size_bytes) -> void
106
+
107
+ # Encodes a raw integer value into the message byte array
108
+ def insert_bits: (Array[Integer] bytes, Integer raw) -> void
109
+
110
+ # Validates the raw integer value before encoding
111
+ def validate_raw_value: (Integer raw) -> void
112
+
113
+ # Validates unsigned values to ensure they're not negative
114
+ def validate_unsigned_value: (Integer raw) -> void
115
+
116
+ # Validates signed values to ensure they fit in the signal's bit range
117
+ def validate_signed_value: (Integer raw) -> void
118
+
119
+ # Processes the raw value for encoding (handles two's complement conversion)
120
+ def process_raw_value: (Integer raw) -> Integer
121
+
122
+ # Writes the processed bits into the message byte array
123
+ def write_bits_to_bytes: (Array[Integer] bytes, Integer raw) -> void
124
+
125
+ # Calculates the bit position for a given bit offset within the signal
126
+ def calculate_bit_position: (Integer bit_offset) -> Integer
127
+
128
+ # Calculates byte and bit indices from an absolute bit position
129
+ def calculate_byte_and_bit_indices: (Integer bit_pos) -> [Integer, Integer]
130
+
131
+ # Validates that a bit position is within the message boundaries
132
+ def validate_bit_position: (Integer bit_pos, Integer bytes_size) -> void
133
+
134
+ # Updates a specific bit within a byte in the message array
135
+ def update_byte_with_bit: (Array[Integer] bytes, Integer byte_index, Integer bit_index, Integer bit) -> void
136
+
137
+ # Extracts the signal value from the message byte array
138
+ def extract_bits: (Array[Integer] bytes) -> Integer
139
+
140
+ # Reads the raw bits from the message byte array
141
+ def read_bits_from_bytes: (Array[Integer] bytes) -> Integer
142
+
143
+ # Validates bit position during extraction to ensure it's within bounds
144
+ def validate_extraction_bounds: (Integer bit_pos, Integer bytes_size) -> void
145
+
146
+ # Converts unsigned value to signed if the signal is signed and the MSB is set
147
+ def convert_to_signed_if_needed: (Integer value) -> Integer
148
+
149
+ # Converts a sawtooth bit number to sequential network bit numbering
150
+ def sawtooth_to_network_bitnum: (Integer bitnum) -> Integer
151
+
152
+ # Returns the first invalid bit position for this signal, if any
153
+ def invalid_bit_position: (Integer max_allowed_bit) -> (Integer | nil)
154
+
155
+ # Calculates the maximum valid bit index for a message size
156
+ def max_allowed_bit_for: (Integer message_size_bytes) -> Integer
157
+ end
158
+ end
@@ -0,0 +1,78 @@
1
+ module CanMessenger
2
+ # Messenger class for CAN bus communication
3
+ class Messenger
4
+ @interface_name: String
5
+ @logger: Logger
6
+ @listening: bool
7
+ @endianness: (String | Symbol)
8
+ @can_fd: bool
9
+ @adapter: untyped
10
+
11
+ # Initializes a new Messenger instance
12
+ def initialize: (
13
+ interface_name: String,
14
+ ?logger: Logger?,
15
+ ?endianness: (String | Symbol),
16
+ ?can_fd: bool,
17
+ ?adapter: untyped
18
+ ) -> void
19
+
20
+ # Sends a CAN message by writing directly to a raw CAN socket
21
+ def send_can_message: (
22
+ id: Integer,
23
+ data: Array[Integer],
24
+ ?extended_id: bool,
25
+ ?can_fd: bool?
26
+ ) -> void
27
+
28
+ # Encodes and sends a CAN message using a DBC definition
29
+ def send_dbc_message: (
30
+ message_name: String,
31
+ signals: Hash[Symbol | String, Numeric],
32
+ ?dbc: CanMessenger::DBC?,
33
+ ?extended_id: bool,
34
+ ?can_fd: bool?
35
+ ) -> void
36
+
37
+ # Continuously listens for CAN messages on the specified interface
38
+ def start_listening: (
39
+ ?filter: (Integer | Range[Integer] | Array[Integer])?,
40
+ ?can_fd: bool?,
41
+ ?dbc: CanMessenger::DBC?
42
+ ) { ({ id: Integer, data: Array[Integer], extended: bool, ?remote: bool, ?dlc: Integer, ?decoded: { name: String, signals: Hash[Symbol, Numeric] }, ?decode_error: { class: String, message: String } }) -> untyped } -> void
43
+
44
+ # Stops the listening loop
45
+ def stop_listening: () -> void
46
+
47
+ private
48
+
49
+ # Opens a socket, yields it, and closes it when done
50
+ def with_socket: (?can_fd: bool?) { (untyped socket) -> untyped } -> void
51
+
52
+ # Processes a single CAN message from socket
53
+ def process_message: (
54
+ untyped socket,
55
+ (Integer | Range[Integer] | Array[Integer])? filter,
56
+ bool can_fd,
57
+ CanMessenger::DBC? dbc
58
+ ) { ({ id: Integer, data: Array[Integer], extended: bool, ?remote: bool, ?dlc: Integer, ?decoded: { name: String, signals: Hash[Symbol, Numeric] }, ?decode_error: { class: String, message: String } }) -> untyped } -> void
59
+
60
+ # Checks whether the given message ID matches the specified filter
61
+ def matches_filter?: (message_id: Integer, filter: (Integer | Range[Integer] | Array[Integer])?) -> bool
62
+
63
+ def validate_filter!: ((Integer | Range[Integer] | Array[Integer])?) -> void
64
+ def validate_range_filter!: (Range[Integer] filter) -> void
65
+ def validate_array_filter!: (Array[Integer] filter) -> void
66
+ def attach_dbc_payload!: (
67
+ { id: Integer, data: Array[Integer], extended: bool, ?remote: bool, ?dlc: Integer, ?decoded: { name: String, signals: Hash[Symbol, Numeric] }, ?decode_error: { class: String, message: String } } message,
68
+ CanMessenger::DBC dbc
69
+ ) -> void
70
+ def normalize_can_id: (Integer id) -> Integer
71
+ def dbc_extended_id?: (Integer id) -> bool
72
+ def normalized_dbc_message: (
73
+ { id: Integer, data: Array[Integer] } encoded,
74
+ extended_id: bool
75
+ ) -> { id: Integer, data: Array[Integer], extended_id: bool }
76
+ def dbc_decode_id: ({ id: Integer, extended: bool, ?remote: bool, ?dlc: Integer } message) -> Integer
77
+ end
78
+ end
@@ -0,0 +1,3 @@
1
+ module CanMessenger
2
+ VERSION: String
3
+ end
@@ -0,0 +1,4 @@
1
+ module CanMessenger
2
+ class Error < StandardError
3
+ end
4
+ end
metadata CHANGED
@@ -1,14 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: can_messenger
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.1
4
+ version: 2.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - fk1018
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: logger
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.6'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.6'
12
26
  description: CanMessenger provides an interface to send and receive messages over
13
27
  the CAN bus, useful for applications requiring CAN communication in Ruby.
14
28
  email:
@@ -27,6 +41,13 @@ files:
27
41
  - lib/can_messenger/dbc.rb
28
42
  - lib/can_messenger/messenger.rb
29
43
  - lib/can_messenger/version.rb
44
+ - sig/can_messenger.rbs
45
+ - sig/can_messenger/adapter/base.rbs
46
+ - sig/can_messenger/adapter/socketcan.rbs
47
+ - sig/can_messenger/constants.rbs
48
+ - sig/can_messenger/dbc.rbs
49
+ - sig/can_messenger/messenger.rbs
50
+ - sig/can_messenger/version.rbs
30
51
  homepage: https://can-messenger.github.io/
31
52
  licenses:
32
53
  - MIT
@@ -43,14 +64,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
43
64
  requirements:
44
65
  - - ">="
45
66
  - !ruby/object:Gem::Version
46
- version: 4.0.1
67
+ version: 4.0.9
47
68
  required_rubygems_version: !ruby/object:Gem::Requirement
48
69
  requirements:
49
70
  - - ">="
50
71
  - !ruby/object:Gem::Version
51
72
  version: '0'
52
73
  requirements: []
53
- rubygems_version: 4.0.3
74
+ rubygems_version: 4.0.20
54
75
  specification_version: 4
55
76
  summary: A simple Ruby wrapper to read and write CAN bus messages.
56
77
  test_files: []