can_messenger 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d26615bdfc66cb0379649692d151278aea8056338510b2d73f144f9d0fe0fe4
4
- data.tar.gz: 1259bc700f8adf69e1de5f73cdd1dcbef783612b081be9757a31e918855e1b2a
3
+ metadata.gz: 8a3eb889a0115b86ec0c286505b92919e55271f7c95e5423ee7216bd49587237
4
+ data.tar.gz: 97806e21ddccdcd8815219de47ed0ba0f6acad5182b878db7b2b19b1aa9cde95
5
5
  SHA512:
6
- metadata.gz: a79e6a61508e0915d1b52e2e31c1b7104597c46db9d21c838952acac87060efe88fb4914d9ad64e4ac7c63287ef32a8b0a927f4b494c0212aed95cdd198ff693
7
- data.tar.gz: 61904213c26ef9b468a4083af58cbf00c95102b5ee841a943b3e559504d324234d3b57d3a1b99a829141a0f629705c6b3240e7d02f8f0081d004b86684088459
6
+ metadata.gz: 61c5455b82b6b707885d0babaaea5643b3e0587f4389850dd51aa5956587ca2ea25a040136d075c5d8a01590e8ab9abf445dd1d34b77b0c8556958cefffe8bbe
7
+ data.tar.gz: 0ec5d24c6612c889f472b201037b533b0ac612a7826dfbc2d45ed218f5f6600d0d8e366c0ce90dd04c28d2c421450af28832bac1984223c0da12b512e3b4ff94
data/README.md CHANGED
@@ -1,20 +1,26 @@
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`:
14
+ Ensure you have `SocketCAN` available on your system. Typically on Linux (e.g., Debian/Ubuntu), you install SocketCAN support with:
15
15
 
16
16
  ```bash
17
- sudo apt install can-utils
17
+ sudo apt-get install net-tools iproute2
18
+ ```
19
+
20
+ Then bring up your CAN interface (e.g., `can0`) using `ip` commands or a tool like `can-utils` (though this gem no longer depends on `cansend`):
21
+
22
+ ```bash
23
+ sudo ip link set can0 up type can bitrate 500000
18
24
  ```
19
25
 
20
26
  To install `can_messenger`, add it to your application's Gemfile:
@@ -55,13 +61,15 @@ To send a message:
55
61
  messenger.send_can_message(id: 0x123, data: [0xDE, 0xAD, 0xBE, 0xEF])
56
62
  ```
57
63
 
64
+ > **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.
65
+
58
66
  ### Receiving CAN Messages
59
67
 
60
68
  To listen for incoming messages, set up a listener:
61
69
 
62
70
  ```ruby
63
71
  messenger.start_listening do |message|
64
- puts "Received: ID=#{message[:id]}, Data=#{message[:data]}"
72
+ puts "Received: ID=#{message[:id]}, Data=#{message[:data]}"
65
73
  end
66
74
  ```
67
75
 
@@ -69,31 +77,29 @@ end
69
77
 
70
78
  The `start_listening` method supports filtering incoming messages based on CAN ID:
71
79
 
72
- Single CAN ID:
73
-
74
- ```ruby
75
- messenger.start_listening(filter: 0x123) do |message|
76
- puts "Received filtered message: #{message}"
77
- end
78
- ```
79
-
80
- Range of CAN IDs:
80
+ - Single CAN ID:
81
81
 
82
- ```ruby
83
- messenger.start_listening(filter: 0x100..0x200) do |message|
84
- puts "Received filtered message: #{message}"
85
- end
82
+ ```ruby
83
+ messenger.start_listening(filter: 0x123) do |message|
84
+ puts "Received filtered message: #{message}"
85
+ end
86
+ ```
86
87
 
87
- ```
88
+ - Range of CAN IDs:
88
89
 
89
- Array of CAN IDs:
90
+ ```ruby
91
+ messenger.start_listening(filter: 0x100..0x200) do |message|
92
+ puts "Received filtered message: #{message}"
93
+ end
94
+ ```
90
95
 
91
- ```ruby
92
- messenger.start_listening(filter: [0x123, 0x456, 0x789]) do |message|
93
- puts "Received filtered message: #{message}"
94
- end
96
+ - Array of CAN IDs:
95
97
 
96
- ```
98
+ ```ruby
99
+ messenger.start_listening(filter: [0x123, 0x456, 0x789]) do |message|
100
+ puts "Received filtered message: #{message}"
101
+ end
102
+ ```
97
103
 
98
104
  ### Stopping the Listener
99
105
 
@@ -103,15 +109,62 @@ To stop listening, use:
103
109
  messenger.stop_listening
104
110
  ```
105
111
 
112
+ ## Important Considerations
113
+
114
+ Before using `can_messenger`, please note the following:
115
+
116
+ - **Environment Requirements:**
117
+
118
+ - **SocketCAN** must be available on your Linux system.
119
+ - **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.
120
+
121
+ - **API Changes (v1.0.0 and later):**
122
+
123
+ - **Keyword Arguments:** The Messenger API now requires keyword arguments. For example, when initializing the Messenger:
124
+
125
+ ```ruby
126
+ messenger = CanMessenger::Messenger.new(interface_name: 'can0')
127
+ ```
128
+
129
+ Similarly, methods like `send_can_message` use named parameters:
130
+
131
+ ```ruby
132
+ messenger.send_can_message(id: 0x123, data: [0xDE, 0xAD, 0xBE, 0xEF])
133
+ ```
134
+
135
+ If upgrading from an earlier version, update your code accordingly.
136
+
137
+ - **Block Requirement for `start_listening`:**
138
+ The `start_listening` method requires a block. If no block is provided, the method logs an error and exits without processing messages:
139
+ ```ruby
140
+ messenger.start_listening do |message|
141
+ puts "Received: #{message}"
142
+ end
143
+ ```
144
+
145
+ - **Threading & Socket Management:**
146
+
147
+ - **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.
148
+ - **Resource Cleanup:** The socket is automatically closed when the listening loop terminates. Stop the listener to avoid resource leaks.
149
+
150
+ - **Logging:**
151
+
152
+ - **Default Logger:** If no logger is provided, logs go to standard output. Provide a custom logger if you want more control.
153
+
154
+ - **CAN Frame Format Assumptions:**
155
+ - 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.
156
+ - 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.
157
+
106
158
  ## Features
107
159
 
108
- - **Send CAN Messages**: Send CAN messages with a specified ID.
160
+ - **Send CAN Messages**: Send CAN messages (up to 8 data bytes).
109
161
  - **Receive CAN Messages**: Continuously listen for messages on a CAN interface.
110
- - **Logging**: Logs errors and events for debugging and troubleshooting.
162
+ - **Filtering**: Optional ID filters for incoming messages (single ID, range, or array).
163
+ - **Logging**: Logs errors and events for debugging/troubleshooting.
111
164
 
112
165
  ## Development
113
166
 
114
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test:rspec` to run the tests.
167
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test:rspec` to execute the test suite.
115
168
 
116
169
  ## Contributing
117
170
 
@@ -119,7 +172,7 @@ Bug reports and pull requests are welcome on GitHub at [https://github.com/fk101
119
172
 
120
173
  ## License
121
174
 
122
- The gem is available as open-source under the terms of the MIT License.
175
+ The gem is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
123
176
 
124
177
  ## Author
125
178
 
@@ -1,4 +1,3 @@
1
- # lib/can_messenger/messenger.rb
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require "socket"
@@ -20,27 +19,30 @@ module CanMessenger
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
41
  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
42
+ with_socket do |socket|
43
+ frame = build_can_frame(id: id, data: data)
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,62 @@ 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.
98
97
  #
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.
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.
110
+ #
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:)
115
+ raise ArgumentError, "CAN data cannot exceed 8 bytes" if data.size > 8
116
+
117
+ # Apply 29-bit mask if extended ID is used
118
+ can_id = id & 0x1FFFFFFF
119
+
120
+ # Pack the ID as 4 bytes in big-endian or little-endian
121
+ id_bytes = @endianness == :big ? [can_id].pack("L>") : [can_id].pack("V")
122
+
123
+ # 1 byte for DLC, then 3 bytes of padding
124
+ dlc_and_pad = [data.size, 0, 0, 0].pack("C*")
125
+
126
+ # Up to 8 data bytes, pad with 0 if fewer
127
+ payload = data.pack("C*").ljust(8, "\x00")
128
+
129
+ # Total 16 bytes
130
+ id_bytes + dlc_and_pad + payload
131
+ end
132
+
133
+ # Processes a single CAN message from `socket`. Applies filter, yields to block if it matches.
101
134
  #
102
135
  # @param socket [Socket] The CAN socket.
103
136
  # @param filter [Integer, Range, Array<Integer>, nil] Optional filter for CAN IDs.
@@ -113,28 +146,10 @@ module CanMessenger
113
146
  @logger.error("Unexpected error in listening loop: #{e.message}")
114
147
  end
115
148
 
116
- # Creates and configures a CAN socket.
149
+ # Reads a frame from the socket and parses it into { id:, data: }, or nil if none is received.
117
150
  #
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.
151
+ # @param socket [Socket]
152
+ # @return [Hash, nil]
138
153
  def receive_message(socket:)
139
154
  frame = socket.recv(FRAME_SIZE)
140
155
  return nil if frame.nil? || frame.size < MIN_FRAME_SIZE
@@ -143,20 +158,24 @@ module CanMessenger
143
158
  rescue IO::WaitReadable
144
159
  nil
145
160
  rescue StandardError => e
146
- @logger.error("Error receiving CAN message on interface #{@can_interface}: #{e}")
161
+ @logger.error("Error receiving CAN message on interface #{@interface_name}: #{e}")
147
162
  nil
148
163
  end
149
164
 
150
- # Parses a raw CAN frame into a message hash.
165
+ # Parses a raw CAN frame into { id: Integer, data: Array<Integer> }, or nil on error.
151
166
  #
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.
167
+ # @param [String] frame
168
+ # @return [Hash, nil]
155
169
  def parse_frame(frame:)
156
170
  return nil unless frame && frame.size >= MIN_FRAME_SIZE
157
171
 
158
- id = frame[0..3].unpack1("L>") & 0x1FFFFFFF
172
+ # Big-endian ID in bytes [0..3]
173
+ id = unpack_frame_id(frame: frame)
174
+
175
+ # DLC is the lower 4 bits of byte 4
159
176
  data_length = frame[4].ord & 0x0F
177
+
178
+ # Data follows at byte index 8, up to data_length bytes
160
179
  data = (frame[MIN_FRAME_SIZE, data_length].unpack("C*") if frame.size >= MIN_FRAME_SIZE + data_length)
161
180
  { id: id, data: data }
162
181
  rescue StandardError => e
@@ -164,18 +183,15 @@ module CanMessenger
164
183
  nil
165
184
  end
166
185
 
186
+ def unpack_frame_id(frame:)
187
+ @endianness == :big ? frame[0..3].unpack1("L>") & 0x1FFFFFFF : frame[0..3].unpack1("V") & 0x1FFFFFFF
188
+ end
189
+
167
190
  # Checks whether the given message ID matches the specified filter.
168
191
  #
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
192
  # @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.
193
+ # @param filter [Integer, Range, Array<Integer>, nil]
194
+ # @return [Boolean]
179
195
  def matches_filter?(message_id:, filter:)
180
196
  case filter
181
197
  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.0"
4
+ VERSION = "1.1.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.0
4
+ version: 1.1.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-11 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.