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 +4 -4
- data/README.md +51 -46
- data/lib/can_messenger/messenger.rb +95 -61
- data/lib/can_messenger/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 67c064d4b8899c012642e83b9c348cf72da13eb136bc81283a2acb7116e79897
|
4
|
+
data.tar.gz: ddb8c322325800be6b03813510e1831348e724967e4f05daa9ac0c2b5b32dad3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3d248a72be8fb4b407fa4b79f42fc291658b39340db84c255440cd33b465c02ca7a2d6bf9274a1b52cae8b4cb3b4890d0d2562238040d2365649c1d7d7e0300e
|
7
|
+
data.tar.gz: 7749d6a7544e43db00dfec4a44a967362b449b78bb657da2f7e3afefc04956a466a9348c9414f05c2b72669858e8cecc27f8ac0b32fa2d6c8b58cbaca492e041
|
data/README.md
CHANGED
@@ -1,22 +1,16 @@
|
|
1
1
|
# CanMessenger
|
2
2
|
|
3
|
-
[](https://badge.fury.io/rb/can_messenger)
|
3
|
+
[](https://badge.fury.io/rb/can_messenger)
|
4
4
|
[](https://github.com/fk1018/can_messenger/actions)
|
5
5
|
[](https://codecov.io/gh/fk1018/can_messenger)
|
6
6
|

|
7
7
|
[](https://opensource.org/licenses/MIT)
|
8
8
|

|
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
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
60
|
+
### Listen to CAN Messages
|
69
61
|
|
70
|
-
|
71
|
-
|
72
|
-
Single CAN ID:
|
62
|
+
To listen for incoming messages, set up a listener:
|
73
63
|
|
74
64
|
```ruby
|
75
|
-
messenger.start_listening
|
76
|
-
puts "Received
|
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
|
-
|
70
|
+
#### Listening with Filters
|
81
71
|
|
82
|
-
|
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
|
-
|
76
|
+
```ruby
|
77
|
+
messenger.start_listening(filter: 0x123) do |message|
|
78
|
+
puts "Received filtered message: #{message}"
|
79
|
+
end
|
80
|
+
```
|
90
81
|
|
91
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
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.
|
137
|
-
- **Resource Cleanup:** The socket is automatically closed when the listening loop terminates.
|
141
|
+
- **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.
|
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:**
|
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
|
-
-
|
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
|
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
|
-
- **
|
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
|
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
|
-
@
|
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
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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 #{@
|
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 #{@
|
78
|
+
@logger.info("Stopped listening on #{@interface_name}")
|
77
79
|
end
|
78
80
|
|
79
81
|
private
|
80
82
|
|
81
|
-
#
|
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
|
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
|
-
#
|
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
|
-
#
|
100
|
-
#
|
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
|
-
#
|
152
|
+
# Reads a frame from the socket and parses it into { id:, data: }, or nil if none is received.
|
117
153
|
#
|
118
|
-
# @
|
119
|
-
|
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 #{@
|
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
|
168
|
+
# Parses a raw CAN frame into { id: Integer, data: Array<Integer> }, or nil on error.
|
151
169
|
#
|
152
|
-
# @param [String] frame
|
153
|
-
# @return [
|
154
|
-
|
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
|
-
|
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
|
-
|
161
|
-
|
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]
|
178
|
-
# @return [Boolean]
|
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
|
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
|
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-
|
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.
|