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 +4 -4
- data/README.md +82 -29
- data/lib/can_messenger/messenger.rb +72 -56
- 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: 8a3eb889a0115b86ec0c286505b92919e55271f7c95e5423ee7216bd49587237
|
4
|
+
data.tar.gz: 97806e21ddccdcd8815219de47ed0ba0f6acad5182b878db7b2b19b1aa9cde95
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 61c5455b82b6b707885d0babaaea5643b3e0587f4389850dd51aa5956587ca2ea25a040136d075c5d8a01590e8ab9abf445dd1d34b77b0c8556958cefffe8bbe
|
7
|
+
data.tar.gz: 0ec5d24c6612c889f472b201037b533b0ac612a7826dfbc2d45ed218f5f6600d0d8e366c0ce90dd04c28d2c421450af28832bac1984223c0da12b512e3b4ff94
|
data/README.md
CHANGED
@@ -1,20 +1,26 @@
|
|
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
|
-
|
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
|
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
|
-
|
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:
|
84
|
-
|
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
|
-
|
90
|
+
```ruby
|
91
|
+
messenger.start_listening(filter: 0x100..0x200) do |message|
|
92
|
+
puts "Received filtered message: #{message}"
|
93
|
+
end
|
94
|
+
```
|
90
95
|
|
91
|
-
|
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
|
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
|
-
- **
|
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
|
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
|
-
@
|
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
41
|
def send_can_message(id:, data:)
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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 #{@
|
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 #{@
|
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.
|
98
97
|
#
|
99
|
-
#
|
100
|
-
|
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
|
-
#
|
149
|
+
# Reads a frame from the socket and parses it into { id:, data: }, or nil if none is received.
|
117
150
|
#
|
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.
|
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 #{@
|
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
|
165
|
+
# Parses a raw CAN frame into { id: Integer, data: Array<Integer> }, or nil on error.
|
151
166
|
#
|
152
|
-
# @param [String] frame
|
153
|
-
# @return [
|
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
|
-
|
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]
|
178
|
-
# @return [Boolean]
|
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
|
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.
|
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-
|
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.
|