can_messenger 1.0.3 → 1.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: c3c1f6bf2353eb4b67b93f9aa9e167a4474b8c65cc1abe2327b520d04695cec2
4
- data.tar.gz: 6afe7242348e786084de8c6d8afe25f18727ac57b2eeea727518ddd77d53330a
3
+ metadata.gz: 67c064d4b8899c012642e83b9c348cf72da13eb136bc81283a2acb7116e79897
4
+ data.tar.gz: ddb8c322325800be6b03813510e1831348e724967e4f05daa9ac0c2b5b32dad3
5
5
  SHA512:
6
- metadata.gz: 7cd537434d6c9f0d41a6a9ad6dd37229bb569057d9b9e259ac301792e71b629df1b66f2ece6c3d5606e7224410d4f42d1c2488894f5341faecb551e46b222140
7
- data.tar.gz: 18620bf7cc07a7694438b0e3da277a3b96d4767cb87d36c5310920dbce5976cc21971e4c3e572869c99ee3a37a7eac7e9996f7948f2d5d2001875b9dead5f4c1
6
+ metadata.gz: 3d248a72be8fb4b407fa4b79f42fc291658b39340db84c255440cd33b465c02ca7a2d6bf9274a1b52cae8b4cb3b4890d0d2562238040d2365649c1d7d7e0300e
7
+ data.tar.gz: 7749d6a7544e43db00dfec4a44a967362b449b78bb657da2f7e3afefc04956a466a9348c9414f05c2b72669858e8cecc27f8ac0b32fa2d6c8b58cbaca492e041
data/README.md CHANGED
@@ -1,22 +1,16 @@
1
1
  # CanMessenger
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/can_messenger.svg?icon=si%3Arubygems&icon_color=%23e77682)](https://badge.fury.io/rb/can_messenger)
3
+ [![Gem Version](https://badge.fury.io/rb/can_messenger.svg?icon=si%3Arubygems&icon_color=%23e77682&123)](https://badge.fury.io/rb/can_messenger)
4
4
  [![Build Status](https://github.com/fk1018/can_messenger/actions/workflows/ruby.yml/badge.svg)](https://github.com/fk1018/can_messenger/actions)
5
5
  [![Test Coverage](https://codecov.io/gh/fk1018/can_messenger/branch/main/graph/badge.svg)](https://codecov.io/gh/fk1018/can_messenger)
6
6
  ![Status](https://img.shields.io/badge/status-stable-green)
7
7
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
8
8
  ![Gem Total Downloads](https://img.shields.io/gem/dt/can_messenger)
9
9
 
10
- `can_messenger` is a Ruby gem that provides an interface for communicating over the CAN bus, allowing users to send and receive CAN messages. This gem is designed for developers who need an easy way to interact with CAN-enabled devices.
10
+ `can_messenger` is a Ruby gem that provides an interface for communicating over the CAN bus, allowing users to send and receive CAN messages `via raw SocketCAN sockets`. This gem is designed for developers who need an easy way to interact with CAN-enabled devices on Linux.
11
11
 
12
12
  ## Installation
13
13
 
14
- This gem relies on `cansend` from the `can-utils` package, which is typically available on Linux-based systems. Make sure to install `can-utils` before using `can_messenger`:
15
-
16
- ```bash
17
- sudo apt install can-utils
18
- ```
19
-
20
14
  To install `can_messenger`, add it to your application's Gemfile:
21
15
 
22
16
  ```ruby
@@ -55,45 +49,51 @@ To send a message:
55
49
  messenger.send_can_message(id: 0x123, data: [0xDE, 0xAD, 0xBE, 0xEF])
56
50
  ```
57
51
 
58
- ### Receiving CAN Messages
52
+ > **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.
59
53
 
60
- To listen for incoming messages, set up a listener:
54
+ If you need to send an extended CAN frame (29-bit ID), set extended_id: true. The gem then sets the Extended Frame Format (EFF) bit automatically:### Receiving CAN Messages
61
55
 
62
56
  ```ruby
63
- messenger.start_listening do |message|
64
- puts "Received: ID=#{message[:id]}, Data=#{message[:data]}"
65
- end
57
+ messenger.send_can_message(id: 0x123456, data: [0x01, 0x02, 0x03], extended_id: true)
66
58
  ```
67
59
 
68
- #### Listening with Filters
60
+ ### Listen to CAN Messages
69
61
 
70
- The `start_listening` method supports filtering incoming messages based on CAN ID:
71
-
72
- Single CAN ID:
62
+ To listen for incoming messages, set up a listener:
73
63
 
74
64
  ```ruby
75
- messenger.start_listening(filter: 0x123) do |message|
76
- puts "Received filtered message: #{message}"
65
+ messenger.start_listening do |msg|
66
+ puts "Received ID=0x#{msg[:id].to_s(16)}, Extended=#{msg[:extended]}, Data=#{msg[:data]}"
77
67
  end
78
68
  ```
79
69
 
80
- Range of CAN IDs:
70
+ #### Listening with Filters
81
71
 
82
- ```ruby
83
- messenger.start_listening(filter: 0x100..0x200) do |message|
84
- puts "Received filtered message: #{message}"
85
- end
72
+ The `start_listening` method supports filtering incoming messages based on CAN ID:
86
73
 
87
- ```
74
+ - Single CAN ID:
88
75
 
89
- Array of CAN IDs:
76
+ ```ruby
77
+ messenger.start_listening(filter: 0x123) do |message|
78
+ puts "Received filtered message: #{message}"
79
+ end
80
+ ```
90
81
 
91
- ```ruby
92
- messenger.start_listening(filter: [0x123, 0x456, 0x789]) do |message|
93
- puts "Received filtered message: #{message}"
94
- end
82
+ - Range of CAN IDs:
95
83
 
96
- ```
84
+ ```ruby
85
+ messenger.start_listening(filter: 0x100..0x200) do |message|
86
+ puts "Received filtered message: #{message}"
87
+ end
88
+ ```
89
+
90
+ - Array of CAN IDs:
91
+
92
+ ```ruby
93
+ messenger.start_listening(filter: [0x123, 0x456, 0x789]) do |message|
94
+ puts "Received filtered message: #{message}"
95
+ end
96
+ ```
97
97
 
98
98
  ### Stopping the Listener
99
99
 
@@ -109,51 +109,56 @@ Before using `can_messenger`, please note the following:
109
109
 
110
110
  - **Environment Requirements:**
111
111
 
112
+ - **SocketCAN** must be available on your Linux system.
112
113
  - **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.
113
114
 
114
115
  - **API Changes (v1.0.0 and later):**
115
116
 
116
- - **Keyword Arguments:** The Messenger API now requires keyword arguments. For example, when initializing the Messenger, use:
117
+ - **Keyword Arguments:** The Messenger API now requires keyword arguments. For example, when initializing the Messenger:
118
+
117
119
  ```ruby
118
120
  messenger = CanMessenger::Messenger.new(interface_name: 'can0')
119
121
  ```
120
- Similarly, methods like `send_can_message` now require named parameters:
122
+
123
+ Similarly, methods like `send_can_message` use named parameters:
124
+
121
125
  ```ruby
122
126
  messenger.send_can_message(id: 0x123, data: [0xDE, 0xAD, 0xBE, 0xEF])
123
127
  ```
124
- If you're upgrading from an earlier version, update your code accordingly.
128
+
129
+ If upgrading from an earlier version, update your code accordingly.
130
+
125
131
  - **Block Requirement for `start_listening`:**
126
- The `start_listening` method now requires a block. If no block is provided, the method logs an error and exits without processing messages. Ensure you pass a block to handle incoming CAN messages:
132
+ The `start_listening` method requires a block. If no block is provided, the method logs an error and exits without processing messages:
127
133
  ```ruby
128
134
  messenger.start_listening do |message|
129
- # Process the message here
130
135
  puts "Received: #{message}"
131
136
  end
132
137
  ```
133
138
 
134
139
  - **Threading & Socket Management:**
135
140
 
136
- - **Blocking Behavior:** The gem uses blocking socket calls and continuously listens for messages. Be sure to manage the listener's lifecycle appropriately,especially if using it in a multi-threaded application. Always call `stop_listening` to gracefully shut down the listener.
137
- - **Resource Cleanup:** The socket is automatically closed when the listening loop terminates. However, you should ensure that your application stops the listener to avoid resource leaks.
141
+ - **Blocking Behavior:** The gem uses blocking socket calls and continuously listens for messages. Manage the listeners lifecycle appropriately, especially in multi-threaded environments. Always call `stop_listening` to gracefully shut down the listener.
142
+ - **Resource Cleanup:** The socket is automatically closed when the listening loop terminates. Stop the listener to avoid resource leaks.
138
143
 
139
144
  - **Logging:**
140
145
 
141
- - **Default Logger:** By default, if no logger is provided, the gem logs to standard output. For more controlled logging, pass a custom logger when initializing the Messenger.
146
+ - **Default Logger:** If no logger is provided, logs go to standard output. Provide a custom logger if you want more control.
142
147
 
143
148
  - **CAN Frame Format Assumptions:**
144
- - The gem expects a standard CAN frame format with a minimum frame size and specific layout (e.g., the first 4 bytes for the CAN ID, followed by a byte indicating data length, etc.). If you work with non-standard frames, you may need to adjust the implementation.
145
-
146
- By keeping these points in mind, you can avoid common pitfalls and ensure that `can_messenger` is integrated smoothly into your project.
149
+ - By default, the gem uses **big-endian** packing for CAN IDs. If you integrate with a system using little-endian, you may need to adjust or specify an endianness in the code.
150
+ - The gem expects a standard CAN frame layout (16 bytes total, with the first 4 for the ID, followed by 1 byte for DLC, 3 bytes of padding, and up to 8 bytes of data). If you work with non-standard frames or CAN FD (64-byte data), you’ll need to customize the parsing/sending logic.
147
151
 
148
152
  ## Features
149
153
 
150
- - **Send CAN Messages**: Send CAN messages with a specified ID.
154
+ - **Send CAN Messages**: Send CAN messages (up to 8 data bytes).
151
155
  - **Receive CAN Messages**: Continuously listen for messages on a CAN interface.
152
- - **Logging**: Logs errors and events for debugging and troubleshooting.
156
+ - **Filtering**: Optional ID filters for incoming messages (single ID, range, or array).
157
+ - **Logging**: Logs errors and events for debugging/troubleshooting.
153
158
 
154
159
  ## Development
155
160
 
156
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test:rspec` to run the tests.
161
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test:rspec` to execute the test suite.
157
162
 
158
163
  ## Contributing
159
164
 
@@ -161,7 +166,7 @@ Bug reports and pull requests are welcome on GitHub at [https://github.com/fk101
161
166
 
162
167
  ## License
163
168
 
164
- The gem is available as open-source under the terms of the MIT License.
169
+ The gem is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
165
170
 
166
171
  ## Author
167
172
 
@@ -1,4 +1,3 @@
1
- # lib/can_messenger/messenger.rb
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require "socket"
@@ -16,31 +15,34 @@ module CanMessenger
16
15
  # messenger.start_listening do |message|
17
16
  # puts "Received: ID=#{message[:id]}, Data=#{message[:data].map { |b| '0x%02X' % b }}"
18
17
  # end
19
- class Messenger
18
+ class Messenger # rubocop:disable Metrics/ClassLength
20
19
  FRAME_SIZE = 16
21
20
  MIN_FRAME_SIZE = 8
22
21
  TIMEOUT = [1, 0].pack("l_2")
22
+
23
23
  # Initializes a new Messenger instance.
24
24
  #
25
25
  # @param [String] interface_name The CAN interface to use (e.g., 'can0').
26
26
  # @param [Logger, nil] logger Optional logger for error handling and debug information.
27
+ # @param [Symbol] endianness The endianness of the CAN ID (default: :big) can be :big or :little.
27
28
  # @return [void]
28
- def initialize(interface_name:, logger: nil)
29
- @can_interface = interface_name
29
+ def initialize(interface_name:, logger: nil, endianness: :big)
30
+ @interface_name = interface_name
30
31
  @logger = logger || Logger.new($stdout)
31
32
  @listening = true # Control flag for listening loop
33
+ @endianness = endianness # :big or :little
32
34
  end
33
35
 
34
- # Sends a CAN message using the `cansend` command.
36
+ # Sends a CAN message by writing directly to a raw CAN socket
35
37
  #
36
- # @param [Integer] id The CAN ID of the message.
37
- # @param [Array<Integer>] data The data bytes of the CAN message.
38
+ # @param [Integer] id The CAN ID of the message (up to 29 bits for extended IDs).
39
+ # @param [Array<Integer>] data The data bytes of the CAN message (0 to 8 bytes).
38
40
  # @return [void]
39
- def send_can_message(id:, data:)
40
- hex_id = format("%03X", id)
41
- hex_data = data.map { |byte| format("%02X", byte) }.join
42
- command = "cansend #{@can_interface} #{hex_id}##{hex_data}"
43
- system(command) # @todo validate command status
41
+ def send_can_message(id:, data:, extended_id: false)
42
+ with_socket do |socket|
43
+ frame = build_can_frame(id: id, data: data, extended_id: extended_id)
44
+ socket.write(frame)
45
+ end
44
46
  rescue StandardError => e
45
47
  @logger.error("Error sending CAN message (ID: #{id}): #{e}")
46
48
  end
@@ -62,7 +64,7 @@ module CanMessenger
62
64
  return @logger.error("No block provided to handle messages.") unless block_given?
63
65
 
64
66
  with_socket do |socket|
65
- @logger.info("Started listening on #{@can_interface}")
67
+ @logger.info("Started listening on #{@interface_name}")
66
68
  process_message(socket, filter, &block) while @listening
67
69
  end
68
70
  end
@@ -73,31 +75,65 @@ module CanMessenger
73
75
  # @return [void]
74
76
  def stop_listening
75
77
  @listening = false
76
- @logger.info("Stopped listening on #{@can_interface}")
78
+ @logger.info("Stopped listening on #{@interface_name}")
77
79
  end
78
80
 
79
81
  private
80
82
 
81
- # Yields an open CAN socket to the given block.
82
- #
83
- # Opens a socket and, if successful, yields it to the block.
84
- # If the socket cannot be opened, logs an error and returns.
83
+ # Opens a socket, yields it, and closes it when done.
85
84
  #
86
85
  # @yield [socket] An open CAN socket.
87
86
  # @return [void]
88
87
  def with_socket
89
88
  socket = open_can_socket
90
- return @logger.error("Failed to open socket, cannot continue listening.") if socket.nil?
89
+ return @logger.error("Failed to open socket, cannot continue operation.") if socket.nil?
91
90
 
92
91
  yield socket
93
92
  ensure
94
93
  socket&.close
95
94
  end
96
95
 
97
- # Processes a single CAN message.
96
+ # Creates and configures a CAN socket bound to @interface_name.
97
+ #
98
+ # @return [Socket, nil] The configured CAN socket, or nil if the socket cannot be opened.
99
+ def open_can_socket
100
+ socket = Socket.open(Socket::PF_CAN, Socket::SOCK_RAW, Socket::CAN_RAW)
101
+ socket.bind(Socket.pack_sockaddr_can(@interface_name))
102
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, TIMEOUT)
103
+ socket
104
+ rescue StandardError => e
105
+ @logger.error("Error creating CAN socket on interface #{@interface_name}: #{e}")
106
+ nil
107
+ end
108
+
109
+ # Builds a raw CAN frame for SocketCAN, big-endian ID, 1-byte DLC, up to 8 data bytes, and 3 padding bytes.
98
110
  #
99
- # Reads a message from the socket, applies the filter, and yields the message if appropriate.
100
- # If an error occurs during processing, it logs the error.
111
+ # @param id [Integer] the CAN ID
112
+ # @param data [Array<Integer>] up to 8 bytes
113
+ # @return [String] a 16-byte string representing a classic CAN frame
114
+ def build_can_frame(id:, data:, extended_id: false)
115
+ raise ArgumentError, "CAN data cannot exceed 8 bytes" if data.size > 8
116
+
117
+ # Mask the ID to 29 bits
118
+ can_id = id & 0x1FFFFFFF
119
+
120
+ # If extended_id == true, set bit 31 (CAN_EFF_FLAG)
121
+ can_id |= 0x80000000 if extended_id
122
+
123
+ # Pack the 4‐byte ID (big-endian or little-endian)
124
+ id_bytes = @endianness == :big ? [can_id].pack("L>") : [can_id].pack("V")
125
+
126
+ # 1 byte for DLC, then 3 bytes of padding
127
+ dlc_and_pad = [data.size, 0, 0, 0].pack("C*")
128
+
129
+ # Up to 8 data bytes, pad with 0 if fewer
130
+ payload = data.pack("C*").ljust(8, "\x00")
131
+
132
+ # Total 16 bytes (4 for ID, 1 for DLC, 3 padding, 8 data)
133
+ id_bytes + dlc_and_pad + payload
134
+ end
135
+
136
+ # Processes a single CAN message from `socket`. Applies filter, yields to block if it matches.
101
137
  #
102
138
  # @param socket [Socket] The CAN socket.
103
139
  # @param filter [Integer, Range, Array<Integer>, nil] Optional filter for CAN IDs.
@@ -113,28 +149,10 @@ module CanMessenger
113
149
  @logger.error("Unexpected error in listening loop: #{e.message}")
114
150
  end
115
151
 
116
- # Creates and configures a CAN socket.
152
+ # Reads a frame from the socket and parses it into { id:, data: }, or nil if none is received.
117
153
  #
118
- # @return [Socket, nil] The configured CAN socket, or nil if the socket cannot be opened.
119
- def open_can_socket
120
- socket = Socket.open(Socket::PF_CAN, Socket::SOCK_RAW, Socket::CAN_RAW)
121
- socket.bind(Socket.pack_sockaddr_can(@can_interface))
122
- socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, TIMEOUT)
123
- socket
124
- rescue StandardError => e
125
- @logger.error("Error creating CAN socket on interface #{@can_interface}: #{e}")
126
- nil
127
- end
128
-
129
- # Receives a CAN message from the given socket and parses it.
130
- #
131
- # This method attempts to read a frame from the provided CAN socket. It returns a parsed
132
- # message hash in the format `{ id: Integer, data: Array<Integer> }` if a valid frame is received.
133
- # If no frame is received, or if an error occurs, the method returns `nil`.
134
- #
135
- # @param socket [Socket] The CAN socket to read from.
136
- # @return [{ id: Integer, data: Array<Integer> }, nil] A hash representing the CAN message, or `nil` if no message
137
- # is received or an error occurs.
154
+ # @param socket [Socket]
155
+ # @return [Hash, nil]
138
156
  def receive_message(socket:)
139
157
  frame = socket.recv(FRAME_SIZE)
140
158
  return nil if frame.nil? || frame.size < MIN_FRAME_SIZE
@@ -143,39 +161,55 @@ module CanMessenger
143
161
  rescue IO::WaitReadable
144
162
  nil
145
163
  rescue StandardError => e
146
- @logger.error("Error receiving CAN message on interface #{@can_interface}: #{e}")
164
+ @logger.error("Error receiving CAN message on interface #{@interface_name}: #{e}")
147
165
  nil
148
166
  end
149
167
 
150
- # Parses a raw CAN frame into a message hash.
168
+ # Parses a raw CAN frame into { id: Integer, data: Array<Integer> }, or nil on error.
151
169
  #
152
- # @param [String] frame The raw CAN frame.
153
- # @return [{ id: Integer, data: Array<Integer> }, nil] Parsed message with :id and :data keys,
154
- # or nil if the frame is incomplete or an error occurs.
155
- def parse_frame(frame:)
170
+ # @param [String] frame
171
+ # @return [Hash, nil]
172
+ def parse_frame(frame:) # rubocop:disable Metrics/MethodLength
156
173
  return nil unless frame && frame.size >= MIN_FRAME_SIZE
157
174
 
158
- id = frame[0..3].unpack1("L>") & 0x1FFFFFFF
175
+ raw_id = unpack_frame_id(frame: frame)
176
+
177
+ # Determine if EFF bit is set
178
+ extended = raw_id.anybits?(0x80000000)
179
+ # or raw_id.anybits?(0x80000000) if your Ruby version supports `Integer#anybits?`
180
+
181
+ # Now mask off everything except the lower 29 bits
182
+ id = raw_id & 0x1FFFFFFF
183
+
184
+ # DLC is the lower 4 bits of byte 4
159
185
  data_length = frame[4].ord & 0x0F
160
- data = (frame[MIN_FRAME_SIZE, data_length].unpack("C*") if frame.size >= MIN_FRAME_SIZE + data_length)
161
- { id: id, data: data }
186
+
187
+ # Extract data
188
+ data = if frame.size >= MIN_FRAME_SIZE + data_length
189
+ frame[MIN_FRAME_SIZE, data_length].unpack("C*")
190
+ else
191
+ []
192
+ end
193
+
194
+ { id: id, data: data, extended: extended }
162
195
  rescue StandardError => e
163
196
  @logger.error("Error parsing CAN frame: #{e}")
164
197
  nil
165
198
  end
166
199
 
200
+ def unpack_frame_id(frame:)
201
+ if @endianness == :big
202
+ frame[0..3].unpack1("L>")
203
+ else
204
+ frame[0..3].unpack1("V")
205
+ end
206
+ end
207
+
167
208
  # Checks whether the given message ID matches the specified filter.
168
209
  #
169
- # The filter can be one of the following:
170
- # - An Integer, which requires an exact match.
171
- # - A Range of Integers, where the message ID must fall within the range.
172
- # - An Array of Integers, where the message ID must be included in the array.
173
- #
174
- # If the filter is nil or unrecognized, the method returns true.
175
- #
176
210
  # @param message_id [Integer] The ID of the incoming CAN message.
177
- # @param filter [Integer, Range, Array<Integer>, nil] The filter to apply.
178
- # @return [Boolean] Returns true if the message ID matches the filter otherwise false.
211
+ # @param filter [Integer, Range, Array<Integer>, nil]
212
+ # @return [Boolean]
179
213
  def matches_filter?(message_id:, filter:)
180
214
  case filter
181
215
  when Integer then message_id == filter
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CanMessenger
4
- VERSION = "1.0.3"
4
+ VERSION = "1.2.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: can_messenger
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - fk1018
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-02-10 00:00:00.000000000 Z
11
+ date: 2025-02-28 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: CanMessenger provides an interface to send and receive messages over
14
14
  the CAN bus, useful for applications requiring CAN communication in Ruby.