can_messenger 1.3.0 → 2.0.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/CHANGELOG.md +165 -0
- data/LICENSE.txt +21 -0
- data/README.md +41 -6
- data/lib/can_messenger/adapter/base.rb +62 -0
- data/lib/can_messenger/adapter/socketcan.rb +125 -0
- data/lib/can_messenger/dbc.rb +515 -0
- data/lib/can_messenger/messenger.rb +41 -131
- data/lib/can_messenger/version.rb +1 -1
- data/lib/can_messenger.rb +1 -0
- metadata +9 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 15f09405e67a906115972dc84790769875d1dcdb5cbd78a57827ccdf910d7e05
|
|
4
|
+
data.tar.gz: faa23fc30c89b5a81875ff2664e5f97cac2970cb6161599887a1c02da9b5f36c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bb33fdda392ca2acaea5357a661b5b9dda245c41db5b000802e4d12886d58b16c49d5238312aa254f062fdc9f46c9f05b9f140f202a0ad73e1ceda9b81d397ca
|
|
7
|
+
data.tar.gz: 4823263138efc7d61756d473e2e6235487921f7d8e6d8350260dcd80d491bee6927cbf5606671a242576fdb3f3fb5437212f77c008856c3bfa1d2b9a5b5d4924
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
## [2.0.0] - 2026-02-02
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **Breaking:** Require Ruby 4.0.1 or higher.
|
|
8
|
+
- Update CI to run on Ruby 4.0.1.
|
|
9
|
+
- Upgrade RuboCop to support Ruby 4.0 and refresh linting.
|
|
10
|
+
- Minor style cleanups in DBC parsing and message listener block forwarding.
|
|
11
|
+
- Extract SocketCAN logic into a dedicated adapter and add a base adapter interface.
|
|
12
|
+
- Allow injecting a custom adapter into `Messenger` for alternate transports or testing.
|
|
13
|
+
- **Breaking:** Default CAN ID endianness is now native (`:native`) instead of `:big`.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Close SocketCAN sockets when bind/setsockopt fails to avoid leaks.
|
|
18
|
+
|
|
19
|
+
## [1.4.0] - 2025-07-25
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- `send_dbc_message` helper for encoding and sending messages defined in DBC files.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- `send_can_message` now only accepts raw frame parameters.
|
|
28
|
+
- DBC parsing code split into helper methods for clarity.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Correct encoding of negative signal values using two's-complement.
|
|
33
|
+
|
|
34
|
+
## [1.3.0] - 2025-06-27
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- Optional **CAN FD** support for sending and receiving up to 64-byte frames.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- Updated APIs to accept a `can_fd:` flag on initialization and message methods.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- (Nothing since last release.)
|
|
47
|
+
|
|
48
|
+
## [1.2.1] - 2025-06-05
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
|
|
52
|
+
- `send_can_message` now raises `ArgumentError` when data length exceeds eight bytes.
|
|
53
|
+
- Updated RBS `initialize` signature to include the `endianness` argument.
|
|
54
|
+
- Fixed formatting in README around extended CAN frames.
|
|
55
|
+
- Clarified spec helper comment.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- Addressed a listener restart bug allowing `start_listening` to be called again.
|
|
60
|
+
|
|
61
|
+
## [1.2.0] - 2025-02-28
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
|
|
65
|
+
- **Explicit extended CAN ID support**.
|
|
66
|
+
- Added an `extended_id: false` parameter to `send_can_message`, which, if set to `true`, sets the Extended Frame Format bit (bit 31) in the CAN ID.
|
|
67
|
+
- Updated `parse_frame` to detect and report `extended: true` when the EFF bit is set in incoming frames.
|
|
68
|
+
- Added corresponding tests for sending and receiving extended CAN frames.
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
|
|
72
|
+
- _No breaking changes_, but internal refactoring around how CAN IDs are packed and unpacked.
|
|
73
|
+
- Removed the masking of bit 31 in `unpack_frame_id`, ensuring extended frames are no longer silently treated as standard frames.
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
|
|
77
|
+
- (Nothing since last release.)
|
|
78
|
+
|
|
79
|
+
## [1.1.0] - 2025-02-10
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
|
|
83
|
+
- **Removed dependency on `cansend`**. We now write CAN frames directly via raw sockets.
|
|
84
|
+
- Internal refactoring to support raw-socket–based sending without changing the public API.
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
|
|
88
|
+
## [1.0.3] - 2025-02-09
|
|
89
|
+
|
|
90
|
+
- Revert release.yml
|
|
91
|
+
|
|
92
|
+
### Fixed
|
|
93
|
+
|
|
94
|
+
## [1.0.2] - 2025-02-09
|
|
95
|
+
|
|
96
|
+
- Bugfix release.yml
|
|
97
|
+
|
|
98
|
+
## [1.0.1] - 2025-02-09
|
|
99
|
+
|
|
100
|
+
### Changed
|
|
101
|
+
|
|
102
|
+
- Updated the README to include an **Important Considerations** section that outlines environment requirements, API changes (keyword arguments and block requirement), threading and socket management notes, and logging behavior.
|
|
103
|
+
- Made minor documentation clarifications and tweaks to help users avoid common pitfalls.
|
|
104
|
+
|
|
105
|
+
## [1.0.0] - 2025-02-09
|
|
106
|
+
|
|
107
|
+
### Changed
|
|
108
|
+
|
|
109
|
+
- **Breaking Change:** Updated the Messenger API to require keyword arguments (e.g., `interface_name:`) for initialization. Existing code using the old API will need to be updated.
|
|
110
|
+
- **Breaking Change:** Updated the send_can_message Method to require keyword arguments (e.g., `id:,data:`) to send messages to the can bus. Existing code using the old API will need to be updated.
|
|
111
|
+
- Refactored `start_listening`.
|
|
112
|
+
- Enhanced error handling throughout the gem with more detailed logging.
|
|
113
|
+
- Updated type signatures (RBS) and documentation to match the new API.
|
|
114
|
+
- Refactored tests to reflect the new API and improved error handling.
|
|
115
|
+
|
|
116
|
+
## [0.2.3] - 2025-02-01
|
|
117
|
+
|
|
118
|
+
### Changed
|
|
119
|
+
|
|
120
|
+
- Updated the internal listening loop in `start_listening` to continue iterating on nil (timeout) responses instead of breaking out, improving reliability.
|
|
121
|
+
- Suppressed log output during tests by injecting a silent logger.
|
|
122
|
+
- Updated the test suite to better handle long-running listening loops and error conditions.
|
|
123
|
+
|
|
124
|
+
## [0.2.2] - 2024-12-06
|
|
125
|
+
|
|
126
|
+
### Changed
|
|
127
|
+
|
|
128
|
+
- Updated README.md to reflect modern debian package install command.
|
|
129
|
+
|
|
130
|
+
## [0.2.1] - 2024-12-06
|
|
131
|
+
|
|
132
|
+
### Changed
|
|
133
|
+
|
|
134
|
+
- Updated `start_listening` RBS signature to include the `filter` parameter, ensuring type definitions match the implementation.
|
|
135
|
+
|
|
136
|
+
## [0.2.0] - 2024-12-05
|
|
137
|
+
|
|
138
|
+
### Added
|
|
139
|
+
|
|
140
|
+
- Filtering support for `start_listening` via a `filter` parameter:
|
|
141
|
+
- Single CAN ID.
|
|
142
|
+
- Range of CAN IDs.
|
|
143
|
+
- Array of CAN IDs.
|
|
144
|
+
|
|
145
|
+
### Changed
|
|
146
|
+
|
|
147
|
+
- Refactored `start_listening` to support optional filtering of incoming CAN messages.
|
|
148
|
+
- Documentation updates for `start_listening` in README.
|
|
149
|
+
|
|
150
|
+
## [0.1.0] - 2024-11-10
|
|
151
|
+
|
|
152
|
+
- Initial release
|
|
153
|
+
[2.0.0]: https://github.com/fk1018/can_messenger/compare/v1.3.0...v2.0.0
|
|
154
|
+
[1.3.0]: https://github.com/fk1018/can_messenger/compare/v1.2.1...v1.3.0
|
|
155
|
+
[1.2.1]: https://github.com/fk1018/can_messenger/compare/v1.2.0...v1.2.1
|
|
156
|
+
[1.2.0]: https://github.com/fk1018/can_messenger/compare/v1.1.0...v1.2.0
|
|
157
|
+
[1.1.0]: https://github.com/fk1018/can_messenger/compare/v1.0.3...v1.1.0
|
|
158
|
+
[1.0.3]: https://github.com/fk1018/can_messenger/compare/v1.0.1...v1.0.3
|
|
159
|
+
[1.0.1]: https://github.com/fk1018/can_messenger/compare/v1.0.0...v1.0.1
|
|
160
|
+
[1.0.0]: https://github.com/fk1018/can_messenger/compare/v0.2.3...v1.0.0
|
|
161
|
+
[0.2.3]: https://github.com/fk1018/can_messenger/compare/v0.2.2...v0.2.3
|
|
162
|
+
[0.2.2]: https://github.com/fk1018/can_messenger/compare/v0.2.1...v0.2.2
|
|
163
|
+
[0.2.1]: https://github.com/fk1018/can_messenger/compare/v0.2.0...v0.2.1
|
|
164
|
+
[0.2.0]: https://github.com/fk1018/can_messenger/compare/v0.1.0...v0.2.0
|
|
165
|
+
[0.1.0]: https://github.com/fk1018/can_messenger/releases/tag/v0.1.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 fk1018
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
CHANGED
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|

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

|
|
9
|
+

|
|
9
10
|
|
|
10
11
|
`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
12
|
|
|
12
13
|
## Requirements
|
|
13
14
|
|
|
14
|
-
- Ruby
|
|
15
|
+
- Ruby 4.0.1 or higher.
|
|
15
16
|
|
|
16
17
|
## Installation
|
|
17
18
|
|
|
@@ -108,6 +109,24 @@ The `start_listening` method supports filtering incoming messages based on CAN I
|
|
|
108
109
|
end
|
|
109
110
|
```
|
|
110
111
|
|
|
112
|
+
### Working with DBC Files
|
|
113
|
+
|
|
114
|
+
Parse a DBC file and let the messenger encode and decode messages automatically:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
dbc = CanMessenger::DBC.load('example.dbc')
|
|
118
|
+
|
|
119
|
+
# Encode using signal values
|
|
120
|
+
messenger.send_dbc_message(dbc: dbc, message_name: 'Example', signals: { Speed: 100 })
|
|
121
|
+
|
|
122
|
+
# Decode received frames
|
|
123
|
+
messenger.start_listening(dbc: dbc) do |msg|
|
|
124
|
+
if msg[:decoded]
|
|
125
|
+
puts "#{msg[:decoded][:name]} => #{msg[:decoded][:signals]}"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
111
130
|
### Stopping the Listener
|
|
112
131
|
|
|
113
132
|
To stop listening, use:
|
|
@@ -116,17 +135,34 @@ To stop listening, use:
|
|
|
116
135
|
messenger.stop_listening
|
|
117
136
|
```
|
|
118
137
|
|
|
138
|
+
### Adapters
|
|
139
|
+
|
|
140
|
+
`CanMessenger::Messenger` delegates low-level CAN bus operations to an adapter. By default it uses the
|
|
141
|
+
SocketCAN adapter which communicates with Linux CAN interfaces using raw sockets:
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
messenger = CanMessenger::Messenger.new(interface_name: "can0")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
You can provide a custom adapter via the `adapter:` option:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
my_adapter = MyCustomAdapter.new(interface_name: "can0", logger: Logger.new($stdout))
|
|
151
|
+
messenger = CanMessenger::Messenger.new(interface_name: "can0", adapter: my_adapter)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
To build your own adapter, subclass `CanMessenger::Adapter::Base` and implement the required methods
|
|
155
|
+
`open_socket`, `build_can_frame`, `receive_message`, and `parse_frame`.
|
|
156
|
+
|
|
119
157
|
## Important Considerations
|
|
120
158
|
|
|
121
159
|
Before using `can_messenger`, please note the following:
|
|
122
160
|
|
|
123
161
|
- **Environment Requirements:**
|
|
124
|
-
|
|
125
162
|
- **SocketCAN** must be available on your Linux system.
|
|
126
163
|
- **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.
|
|
127
164
|
|
|
128
165
|
- **API Changes (v1.0.0 and later):**
|
|
129
|
-
|
|
130
166
|
- **Keyword Arguments:** The Messenger API now requires keyword arguments. For example, when initializing the Messenger:
|
|
131
167
|
|
|
132
168
|
```ruby
|
|
@@ -150,16 +186,14 @@ Before using `can_messenger`, please note the following:
|
|
|
150
186
|
```
|
|
151
187
|
|
|
152
188
|
- **Threading & Socket Management:**
|
|
153
|
-
|
|
154
189
|
- **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.
|
|
155
190
|
- **Resource Cleanup:** The socket is automatically closed when the listening loop terminates. Stop the listener to avoid resource leaks.
|
|
156
191
|
|
|
157
192
|
- **Logging:**
|
|
158
|
-
|
|
159
193
|
- **Default Logger:** If no logger is provided, logs go to standard output. Provide a custom logger if you want more control.
|
|
160
194
|
|
|
161
195
|
- **CAN Frame Format Assumptions:**
|
|
162
|
-
- By default, the gem uses **
|
|
196
|
+
- By default, the gem uses **native endianness** for CAN IDs (little-endian on most x86/ARM systems). **Changed in v2.0.0:** this default was previously `:big`. You can override this by passing `endianness: :big` or `endianness: :little`.
|
|
163
197
|
- 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). **CAN FD** frames (up to 64 bytes) are supported when enabled.
|
|
164
198
|
|
|
165
199
|
## Features
|
|
@@ -168,6 +202,7 @@ Before using `can_messenger`, please note the following:
|
|
|
168
202
|
- **Receive CAN Messages**: Continuously listen for messages on a CAN interface.
|
|
169
203
|
- **Filtering**: Optional ID filters for incoming messages (single ID, range, or array).
|
|
170
204
|
- **Logging**: Logs errors and events for debugging/troubleshooting.
|
|
205
|
+
- **DBC Parsing**: Parse DBC files to encode messages by name and decode incoming frames.
|
|
171
206
|
|
|
172
207
|
## Development
|
|
173
208
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CanMessenger
|
|
4
|
+
module Adapter
|
|
5
|
+
# Base adapter defines the interface for CAN bus adapters.
|
|
6
|
+
# Concrete adapters must implement all of the methods defined here.
|
|
7
|
+
class Base
|
|
8
|
+
attr_reader :interface_name, :logger, :endianness
|
|
9
|
+
|
|
10
|
+
def self.native_endianness
|
|
11
|
+
native = pack_uint(1, "I")
|
|
12
|
+
return :little if native == pack_uint(1, "V")
|
|
13
|
+
return :big if native == pack_uint(1, "N")
|
|
14
|
+
|
|
15
|
+
# Fallback for unusual platforms.
|
|
16
|
+
native.bytes.first == 1 ? :little : :big
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.pack_uint(value, template)
|
|
20
|
+
[value].pack(template)
|
|
21
|
+
end
|
|
22
|
+
private_class_method :pack_uint
|
|
23
|
+
|
|
24
|
+
def initialize(interface_name:, logger:, endianness: :native)
|
|
25
|
+
@interface_name = interface_name
|
|
26
|
+
@logger = logger
|
|
27
|
+
normalized_endianness = normalize_endianness(endianness)
|
|
28
|
+
@endianness = normalized_endianness == :native ? self.class.native_endianness : normalized_endianness
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Open a socket for the underlying interface.
|
|
32
|
+
# @return [Object] adapter-specific socket
|
|
33
|
+
def open_socket(can_fd: false)
|
|
34
|
+
raise NotImplementedError, "open_socket must be implemented in subclasses"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Build a frame ready to be written to the socket.
|
|
38
|
+
def build_can_frame(id:, data:, extended_id: false, can_fd: false)
|
|
39
|
+
raise NotImplementedError, "build_can_frame must be implemented in subclasses"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Receive and parse a frame from the socket.
|
|
43
|
+
def receive_message(socket:, can_fd: false)
|
|
44
|
+
raise NotImplementedError, "receive_message must be implemented in subclasses"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Parse a raw frame string into a message hash.
|
|
48
|
+
def parse_frame(frame:, can_fd: false)
|
|
49
|
+
raise NotImplementedError, "parse_frame must be implemented in subclasses"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def normalize_endianness(endianness)
|
|
55
|
+
normalized = endianness.is_a?(String) ? endianness.strip.downcase.to_sym : endianness
|
|
56
|
+
return normalized if %i[native little big].include?(normalized)
|
|
57
|
+
|
|
58
|
+
raise ArgumentError, "endianness must be :native, :little, or :big"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
|
|
6
|
+
module CanMessenger
|
|
7
|
+
module Adapter
|
|
8
|
+
# Adapter implementation for Linux SocketCAN interfaces.
|
|
9
|
+
class Socketcan < Base
|
|
10
|
+
FRAME_SIZE = 16
|
|
11
|
+
CANFD_FRAME_SIZE = 72
|
|
12
|
+
MIN_FRAME_SIZE = 8
|
|
13
|
+
MAX_FD_DATA = 64
|
|
14
|
+
TIMEOUT = [1, 0].pack("l_2")
|
|
15
|
+
|
|
16
|
+
# Creates and configures a CAN socket bound to the interface.
|
|
17
|
+
def open_socket(can_fd: false)
|
|
18
|
+
socket = Socket.open(Socket::PF_CAN, Socket::SOCK_RAW, Socket::CAN_RAW)
|
|
19
|
+
configure_socket(socket, can_fd: can_fd)
|
|
20
|
+
socket
|
|
21
|
+
rescue StandardError => e
|
|
22
|
+
close_socket(socket)
|
|
23
|
+
logger.error("Error creating CAN socket on interface #{interface_name}: #{e}")
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Builds a raw CAN or CAN FD frame for SocketCAN.
|
|
28
|
+
def build_can_frame(id:, data:, extended_id: false, can_fd: false) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
29
|
+
if can_fd
|
|
30
|
+
raise ArgumentError, "CAN FD data cannot exceed #{MAX_FD_DATA} bytes" if data.size > MAX_FD_DATA
|
|
31
|
+
elsif data.size > 8
|
|
32
|
+
raise ArgumentError, "CAN data cannot exceed 8 bytes"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Mask the ID to 29 bits
|
|
36
|
+
can_id = id & 0x1FFFFFFF
|
|
37
|
+
# Set bit 31 for extended frames
|
|
38
|
+
can_id |= 0x80000000 if extended_id
|
|
39
|
+
|
|
40
|
+
# Pack the ID based on endianness
|
|
41
|
+
id_bytes = endianness == :big ? [can_id].pack("L>") : [can_id].pack("V")
|
|
42
|
+
|
|
43
|
+
dlc_and_pad = [data.size, 0, 0, 0].pack("C*")
|
|
44
|
+
|
|
45
|
+
payload = if can_fd
|
|
46
|
+
data.pack("C*").ljust(MAX_FD_DATA, "\x00")
|
|
47
|
+
else
|
|
48
|
+
data.pack("C*").ljust(8, "\x00")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
id_bytes + dlc_and_pad + payload
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Reads a frame from the socket and parses it into a hash.
|
|
55
|
+
def receive_message(socket:, can_fd: false)
|
|
56
|
+
frame_size = can_fd ? CANFD_FRAME_SIZE : FRAME_SIZE
|
|
57
|
+
frame = socket.recv(frame_size)
|
|
58
|
+
return nil if frame.nil? || frame.size < MIN_FRAME_SIZE
|
|
59
|
+
|
|
60
|
+
parse_frame(frame: frame, can_fd: can_fd)
|
|
61
|
+
rescue IO::WaitReadable
|
|
62
|
+
nil
|
|
63
|
+
rescue StandardError => e
|
|
64
|
+
logger.error("Error receiving CAN message on interface #{interface_name}: #{e}")
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Parses a raw CAN frame string into a hash with id, data and extended flag.
|
|
69
|
+
def parse_frame(frame:, can_fd: nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
70
|
+
return nil unless frame && frame.size >= MIN_FRAME_SIZE
|
|
71
|
+
|
|
72
|
+
use_fd = can_fd.nil? ? frame.size >= CANFD_FRAME_SIZE : can_fd
|
|
73
|
+
|
|
74
|
+
raw_id = unpack_frame_id(frame: frame)
|
|
75
|
+
extended = raw_id.anybits?(0x80000000)
|
|
76
|
+
id = raw_id & 0x1FFFFFFF
|
|
77
|
+
|
|
78
|
+
data_length = if use_fd
|
|
79
|
+
frame[4].ord
|
|
80
|
+
else
|
|
81
|
+
frame[4].ord & 0x0F
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
data = if frame.size >= MIN_FRAME_SIZE + data_length
|
|
85
|
+
frame[MIN_FRAME_SIZE, data_length].unpack("C*")
|
|
86
|
+
else
|
|
87
|
+
[]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
{ id: id, data: data, extended: extended }
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
logger.error("Error parsing CAN frame: #{e}")
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def unpack_frame_id(frame:)
|
|
99
|
+
if endianness == :big
|
|
100
|
+
frame[0..3].unpack1("L>")
|
|
101
|
+
else
|
|
102
|
+
frame[0..3].unpack1("V")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def configure_socket(socket, can_fd:)
|
|
107
|
+
socket.bind(Socket.pack_sockaddr_can(interface_name))
|
|
108
|
+
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, TIMEOUT)
|
|
109
|
+
return unless can_fd && Socket.const_defined?(:CAN_RAW_FD_FRAMES)
|
|
110
|
+
|
|
111
|
+
socket.setsockopt(Socket.const_defined?(:SOL_CAN_RAW) ? Socket::SOL_CAN_RAW : Socket::CAN_RAW,
|
|
112
|
+
Socket::CAN_RAW_FD_FRAMES, 1)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def close_socket(socket)
|
|
116
|
+
return unless socket
|
|
117
|
+
return if socket.closed?
|
|
118
|
+
|
|
119
|
+
socket.close
|
|
120
|
+
rescue StandardError
|
|
121
|
+
# Ignore close errors so we can report the original failure.
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CanMessenger
|
|
4
|
+
# DBC (Database CAN) Parser and Encoder/Decoder
|
|
5
|
+
#
|
|
6
|
+
# This class provides functionality to parse DBC files and encode/decode CAN messages
|
|
7
|
+
# according to the signal definitions. DBC files are a standard way to describe
|
|
8
|
+
# CAN network communication.
|
|
9
|
+
#
|
|
10
|
+
# @example Loading and using a DBC file
|
|
11
|
+
# dbc = CanMessenger::DBC.load('vehicle.dbc')
|
|
12
|
+
#
|
|
13
|
+
# # Encode a message with signal values
|
|
14
|
+
# frame = dbc.encode_can('EngineData', RPM: 2500, Temperature: 85.5)
|
|
15
|
+
# # => { id: 0x123, data: [0x09, 0xC4, 0xAB, 0x00, 0x00, 0x00, 0x00, 0x00] }
|
|
16
|
+
#
|
|
17
|
+
# # Decode a received CAN frame
|
|
18
|
+
# decoded = dbc.decode_can(0x123, [0x09, 0xC4, 0xAB, 0x00, 0x00, 0x00, 0x00, 0x00])
|
|
19
|
+
# # => { name: 'EngineData', signals: { RPM: 2500.0, Temperature: 85.5 } }
|
|
20
|
+
class DBC
|
|
21
|
+
attr_reader :messages
|
|
22
|
+
|
|
23
|
+
# Loads a DBC file from disk and parses its contents.
|
|
24
|
+
#
|
|
25
|
+
# @param [String] path The filesystem path to the DBC file
|
|
26
|
+
# @return [DBC] A new DBC instance with parsed message definitions
|
|
27
|
+
# @raise [Errno::ENOENT] If the file doesn't exist
|
|
28
|
+
# @raise [ArgumentError] If the file contains invalid DBC syntax
|
|
29
|
+
def self.load(path)
|
|
30
|
+
new(File.read(path))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Initializes a new DBC instance.
|
|
34
|
+
#
|
|
35
|
+
# @param [String] content The DBC file content to parse (optional)
|
|
36
|
+
def initialize(content = "")
|
|
37
|
+
@messages = {}
|
|
38
|
+
parse(content) unless content.empty?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Parses DBC content and populates the messages hash.
|
|
42
|
+
#
|
|
43
|
+
# This method processes each line of the DBC content, identifying message
|
|
44
|
+
# definitions (BO_) and signal definitions (SG_). It builds a complete
|
|
45
|
+
# message structure with all associated signals.
|
|
46
|
+
#
|
|
47
|
+
# @param [String] content The DBC file content to parse
|
|
48
|
+
# @return [void]
|
|
49
|
+
def parse(content) # rubocop:disable Metrics/MethodLength
|
|
50
|
+
current = nil
|
|
51
|
+
content.each_line do |line|
|
|
52
|
+
line.strip!
|
|
53
|
+
next if line.empty? || line.start_with?("BO_TX_BU_")
|
|
54
|
+
|
|
55
|
+
if (msg = parse_message_line(line))
|
|
56
|
+
current = msg
|
|
57
|
+
@messages[msg.name] = msg
|
|
58
|
+
elsif current && (sig = parse_signal_line(line, current))
|
|
59
|
+
current.signals << sig
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Parses a message definition line from DBC content.
|
|
65
|
+
#
|
|
66
|
+
# Message lines follow the format: BO_ <ID> <Name>: <DLC> <Node>
|
|
67
|
+
#
|
|
68
|
+
# @param [String] line A single line from the DBC file
|
|
69
|
+
# @return [Message, nil] A Message object if the line matches, nil otherwise
|
|
70
|
+
def parse_message_line(line)
|
|
71
|
+
return unless (m = line.match(/^BO_\s+(\d+)\s+(\w+)\s*:\s*(\d+)/))
|
|
72
|
+
|
|
73
|
+
id = m[1].to_i
|
|
74
|
+
name = m[2]
|
|
75
|
+
dlc = m[3].to_i
|
|
76
|
+
Message.new(id, name, dlc)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Parses a signal definition line from DBC content.
|
|
80
|
+
#
|
|
81
|
+
# Signal lines follow the format:
|
|
82
|
+
# SG_ <SignalName> : <StartBit>|<Length>@<Endianness><Sign> (<Factor>,<Offset>) [<Min>|<Max>] "<Unit>" <Receivers>
|
|
83
|
+
#
|
|
84
|
+
# @param [String] line A single line from the DBC file
|
|
85
|
+
# @param [Message] _current The current message being processed (unused but kept for API consistency)
|
|
86
|
+
# @return [Signal, nil] A Signal object if the line matches, nil otherwise
|
|
87
|
+
def parse_signal_line(line, _current) # rubocop:disable Metrics/MethodLength
|
|
88
|
+
return unless (m = line.match(/^SG_\s+(\w+)\s*:\s*(\d+)\|(\d+)@(\d)([+-])\s*\(([^,]+),([^)]+)\)/))
|
|
89
|
+
|
|
90
|
+
sig_name = m[1]
|
|
91
|
+
start_bit = m[2].to_i
|
|
92
|
+
length = m[3].to_i
|
|
93
|
+
endian = m[4] == "1" ? :little : :big
|
|
94
|
+
sign = m[5] == "-" ? :signed : :unsigned
|
|
95
|
+
factor = m[6].to_f
|
|
96
|
+
offset = m[7].to_f
|
|
97
|
+
|
|
98
|
+
Signal.new(
|
|
99
|
+
sig_name,
|
|
100
|
+
start_bit: start_bit,
|
|
101
|
+
length: length,
|
|
102
|
+
endianness: endian,
|
|
103
|
+
sign: sign,
|
|
104
|
+
factor: factor,
|
|
105
|
+
offset: offset
|
|
106
|
+
)
|
|
107
|
+
end # rubocop:enable Metrics/MethodLength
|
|
108
|
+
|
|
109
|
+
# Encodes signal values into a CAN message frame.
|
|
110
|
+
#
|
|
111
|
+
# Takes a message name and a hash of signal values, then encodes them
|
|
112
|
+
# into the appropriate byte array according to the DBC signal definitions.
|
|
113
|
+
#
|
|
114
|
+
# @param [String] name The name of the message to encode
|
|
115
|
+
# @param [Hash<Symbol|String, Numeric>] values Signal names mapped to their values
|
|
116
|
+
# @return [Hash] A hash containing :id (Integer) and :data (Array<Integer>)
|
|
117
|
+
# @raise [ArgumentError] If the message name is not found in the DBC
|
|
118
|
+
#
|
|
119
|
+
# @example
|
|
120
|
+
# frame = dbc.encode_can('EngineData', RPM: 2500, Temperature: 85.5)
|
|
121
|
+
# # => { id: 0x123, data: [0x09, 0xC4, 0xAB, 0x00, 0x00, 0x00, 0x00, 0x00] }
|
|
122
|
+
def encode_can(name, values)
|
|
123
|
+
msg = @messages[name]
|
|
124
|
+
raise ArgumentError, "Unknown message #{name}" unless msg
|
|
125
|
+
|
|
126
|
+
{ id: msg.id, data: msg.encode(values) }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Decodes a CAN message frame into signal values.
|
|
130
|
+
#
|
|
131
|
+
# Takes a CAN ID and data bytes, finds the matching message definition,
|
|
132
|
+
# and decodes the data into individual signal values according to the DBC.
|
|
133
|
+
#
|
|
134
|
+
# @param [Integer] id The CAN message ID
|
|
135
|
+
# @param [Array<Integer>] data The CAN message data bytes
|
|
136
|
+
# @return [Hash, nil] A hash containing :name (String) and :signals (Hash), or nil if no matching message
|
|
137
|
+
#
|
|
138
|
+
# @example
|
|
139
|
+
# decoded = dbc.decode_can(0x123, [0x09, 0xC4, 0xAB, 0x00, 0x00, 0x00, 0x00, 0x00])
|
|
140
|
+
# # => { name: 'EngineData', signals: { RPM: 2500.0, Temperature: 85.5 } }
|
|
141
|
+
def decode_can(id, data)
|
|
142
|
+
msg = @messages.values.find { |m| m.id == id }
|
|
143
|
+
return nil unless msg
|
|
144
|
+
|
|
145
|
+
{ name: msg.name, signals: msg.decode(data) }
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Represents a CAN message definition from a DBC file.
|
|
150
|
+
#
|
|
151
|
+
# A Message contains the basic message properties (ID, name, data length)
|
|
152
|
+
# and a collection of Signal objects that define how data is structured
|
|
153
|
+
# within the message payload.
|
|
154
|
+
#
|
|
155
|
+
# @example
|
|
156
|
+
# message = Message.new(0x123, 'EngineData', 8)
|
|
157
|
+
# message.signals << Signal.new('RPM', start_bit: 0, length: 16, ...)
|
|
158
|
+
class Message
|
|
159
|
+
attr_reader :id, :name, :dlc, :signals
|
|
160
|
+
|
|
161
|
+
# Initializes a new Message instance.
|
|
162
|
+
#
|
|
163
|
+
# @param [Integer] id The CAN message ID (11-bit standard or 29-bit extended)
|
|
164
|
+
# @param [String] name The symbolic name of the message
|
|
165
|
+
# @param [Integer] dlc Data Length Code - number of bytes in the message (0-8 for classic CAN)
|
|
166
|
+
def initialize(id, name, dlc)
|
|
167
|
+
@id = id
|
|
168
|
+
@name = name
|
|
169
|
+
@dlc = dlc
|
|
170
|
+
@signals = []
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Encodes signal values into the message byte array.
|
|
174
|
+
#
|
|
175
|
+
# Iterates through all signals in the message and encodes their values
|
|
176
|
+
# into the appropriate bit positions within the message data bytes.
|
|
177
|
+
#
|
|
178
|
+
# @param [Hash<Symbol|String, Numeric>] values Signal names mapped to their values
|
|
179
|
+
# @return [Array<Integer>] Array of bytes representing the encoded message
|
|
180
|
+
def encode(values)
|
|
181
|
+
bytes = Array.new(@dlc, 0)
|
|
182
|
+
@signals.each do |sig|
|
|
183
|
+
next unless values.key?(sig.name.to_sym) || values.key?(sig.name.to_s)
|
|
184
|
+
|
|
185
|
+
v = values[sig.name.to_sym] || values[sig.name.to_s]
|
|
186
|
+
sig.encode(bytes, v)
|
|
187
|
+
end
|
|
188
|
+
bytes
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Decodes message data bytes into individual signal values.
|
|
192
|
+
#
|
|
193
|
+
# Extracts and decodes each signal from the message data bytes,
|
|
194
|
+
# applying the appropriate scaling (factor/offset) to produce
|
|
195
|
+
# the final engineering unit values.
|
|
196
|
+
#
|
|
197
|
+
# @param [Array<Integer>] data The message data bytes to decode
|
|
198
|
+
# @return [Hash<Symbol, Float>] Signal names mapped to their decoded values
|
|
199
|
+
def decode(data)
|
|
200
|
+
res = {}
|
|
201
|
+
@signals.each do |sig|
|
|
202
|
+
res[sig.name.to_sym] = sig.decode(data)
|
|
203
|
+
end
|
|
204
|
+
res
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Represents a signal within a CAN message.
|
|
209
|
+
#
|
|
210
|
+
# A Signal defines how a piece of data is encoded within a CAN message,
|
|
211
|
+
# including its bit position, length, byte order, signedness, and scaling.
|
|
212
|
+
# Signals can represent physical values (like temperature, speed) that are
|
|
213
|
+
# encoded as integers in the CAN frame but scaled to engineering units.
|
|
214
|
+
#
|
|
215
|
+
# @example Creating a signal for engine RPM
|
|
216
|
+
# rpm_signal = Signal.new('RPM',
|
|
217
|
+
# start_bit: 0, # Starting at bit 0
|
|
218
|
+
# length: 16, # 16 bits long
|
|
219
|
+
# endianness: :little, # Little-endian byte order
|
|
220
|
+
# sign: :unsigned, # Unsigned integer
|
|
221
|
+
# factor: 0.25, # Scale by 0.25
|
|
222
|
+
# offset: 0 # No offset
|
|
223
|
+
# )
|
|
224
|
+
class Signal # rubocop:disable Metrics/ClassLength
|
|
225
|
+
attr_reader :name, :start_bit, :length, :endianness, :sign, :factor, :offset
|
|
226
|
+
|
|
227
|
+
# Initializes a new Signal instance.
|
|
228
|
+
#
|
|
229
|
+
# @param [String] name The signal name
|
|
230
|
+
# @param [Integer] start_bit The starting bit position within the message (0-based)
|
|
231
|
+
# @param [Integer] length The number of bits the signal occupies (1-64)
|
|
232
|
+
# @param [Symbol] endianness Byte order - :little for little-endian, :big for big-endian
|
|
233
|
+
# @param [Symbol] sign Value type - :unsigned for unsigned integers, :signed for signed integers
|
|
234
|
+
# @param [Float] factor Scaling factor to convert raw value to engineering units
|
|
235
|
+
# @param [Float] offset Offset to add after scaling
|
|
236
|
+
def initialize(name, start_bit:, length:, endianness:, sign:, factor:, offset:) # rubocop:disable Metrics/ParameterLists
|
|
237
|
+
@name = name
|
|
238
|
+
@start_bit = start_bit
|
|
239
|
+
@length = length
|
|
240
|
+
@endianness = endianness
|
|
241
|
+
@sign = sign
|
|
242
|
+
@factor = factor
|
|
243
|
+
@offset = offset
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Encodes a value into the message byte array at this signal's bit position.
|
|
247
|
+
#
|
|
248
|
+
# Converts the engineering unit value to a raw integer using the signal's
|
|
249
|
+
# factor and offset, then places the bits into the appropriate positions
|
|
250
|
+
# within the message bytes.
|
|
251
|
+
#
|
|
252
|
+
# @param [Array<Integer>] bytes The message byte array to modify
|
|
253
|
+
# @param [Numeric] value The engineering unit value to encode
|
|
254
|
+
# @return [void]
|
|
255
|
+
# @raise [ArgumentError] If the value is out of range or signal exceeds message bounds
|
|
256
|
+
def encode(bytes, value)
|
|
257
|
+
raw = ((value - offset) / factor).round
|
|
258
|
+
validate_signal_bounds(bytes.size)
|
|
259
|
+
insert_bits(bytes, raw)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Decodes this signal's value from the message byte array.
|
|
263
|
+
#
|
|
264
|
+
# Extracts the raw integer value from the appropriate bit positions,
|
|
265
|
+
# then applies the signal's scaling (factor and offset) to convert
|
|
266
|
+
# it to engineering units.
|
|
267
|
+
#
|
|
268
|
+
# @param [Array<Integer>] bytes The message byte array to decode from
|
|
269
|
+
# @return [Float] The decoded value in engineering units
|
|
270
|
+
def decode(bytes)
|
|
271
|
+
raw = extract_bits(bytes)
|
|
272
|
+
(raw * factor) + offset
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
private
|
|
276
|
+
|
|
277
|
+
# Validates that the signal fits within the message boundaries.
|
|
278
|
+
#
|
|
279
|
+
# Ensures that all bits used by this signal fall within the message's
|
|
280
|
+
# data length code (DLC) boundaries.
|
|
281
|
+
#
|
|
282
|
+
# @param [Integer] message_size_bytes The size of the message in bytes
|
|
283
|
+
# @return [void]
|
|
284
|
+
# @raise [ArgumentError] If signal bits exceed message boundaries or start_bit is negative
|
|
285
|
+
def validate_signal_bounds(message_size_bytes)
|
|
286
|
+
max_bit = start_bit + length - 1
|
|
287
|
+
max_allowed_bit = (message_size_bytes * 8) - 1
|
|
288
|
+
|
|
289
|
+
raise ArgumentError, "Signal #{name}: start_bit (#{start_bit}) cannot be negative" if start_bit.negative?
|
|
290
|
+
|
|
291
|
+
return unless max_bit > max_allowed_bit
|
|
292
|
+
|
|
293
|
+
raise ArgumentError,
|
|
294
|
+
"Signal #{name}: signal bits #{start_bit}..#{max_bit} exceed message size " \
|
|
295
|
+
"(#{message_size_bytes} bytes = #{max_allowed_bit + 1} bits)"
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Encodes a raw integer value into the message byte array.
|
|
299
|
+
#
|
|
300
|
+
# This is the main encoding method that coordinates validation,
|
|
301
|
+
# value processing, and bit manipulation.
|
|
302
|
+
#
|
|
303
|
+
# @param [Array<Integer>] bytes The message byte array to modify
|
|
304
|
+
# @param [Integer] raw The raw integer value to encode
|
|
305
|
+
# @return [void]
|
|
306
|
+
def insert_bits(bytes, raw)
|
|
307
|
+
validate_raw_value(raw)
|
|
308
|
+
processed_raw = process_raw_value(raw)
|
|
309
|
+
write_bits_to_bytes(bytes, processed_raw)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Validates the raw integer value before encoding.
|
|
313
|
+
#
|
|
314
|
+
# Performs range checking for both signed and unsigned values
|
|
315
|
+
# to ensure they fit within the signal's bit length.
|
|
316
|
+
#
|
|
317
|
+
# @param [Integer] raw The raw value to validate
|
|
318
|
+
# @return [void]
|
|
319
|
+
# @raise [ArgumentError] If the value is out of range for the signal type
|
|
320
|
+
def validate_raw_value(raw)
|
|
321
|
+
validate_unsigned_value(raw)
|
|
322
|
+
validate_signed_value(raw)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Validates unsigned values to ensure they're not negative.
|
|
326
|
+
#
|
|
327
|
+
# @param [Integer] raw The raw value to validate
|
|
328
|
+
# @return [void]
|
|
329
|
+
# @raise [ArgumentError] If an unsigned value is negative
|
|
330
|
+
def validate_unsigned_value(raw)
|
|
331
|
+
return unless sign == :unsigned && raw.negative?
|
|
332
|
+
|
|
333
|
+
raise ArgumentError, "Unsigned value cannot be negative: #{raw}"
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Validates signed values to ensure they fit in the signal's bit range.
|
|
337
|
+
#
|
|
338
|
+
# For signed signals, checks that the value fits within the two's complement
|
|
339
|
+
# range defined by the signal's bit length.
|
|
340
|
+
#
|
|
341
|
+
# @param [Integer] raw The raw value to validate
|
|
342
|
+
# @return [void]
|
|
343
|
+
# @raise [ArgumentError] If a signed value exceeds the bit field's range
|
|
344
|
+
def validate_signed_value(raw)
|
|
345
|
+
return unless sign == :signed
|
|
346
|
+
|
|
347
|
+
min_val = -(1 << (length - 1))
|
|
348
|
+
max_val = (1 << (length - 1)) - 1
|
|
349
|
+
return if raw.between?(min_val, max_val)
|
|
350
|
+
|
|
351
|
+
raise ArgumentError,
|
|
352
|
+
"Signed value #{raw} out of range [#{min_val}..#{max_val}] for #{length}-bit field"
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Processes the raw value for encoding (handles two's complement conversion).
|
|
356
|
+
#
|
|
357
|
+
# For signed negative values, converts them to two's complement representation.
|
|
358
|
+
# Ensures the final value fits within the signal's bit length.
|
|
359
|
+
#
|
|
360
|
+
# @param [Integer] raw The raw value to process
|
|
361
|
+
# @return [Integer] The processed value ready for bit manipulation
|
|
362
|
+
def process_raw_value(raw)
|
|
363
|
+
# Handle signed values: convert negative to two's complement
|
|
364
|
+
raw = (1 << length) + raw if sign == :signed && raw.negative?
|
|
365
|
+
# Ensure the value fits in the specified bit length
|
|
366
|
+
raw & ((1 << length) - 1)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Writes the processed bits into the message byte array.
|
|
370
|
+
#
|
|
371
|
+
# Iterates through each bit of the signal and places it in the correct
|
|
372
|
+
# position within the message bytes, respecting the signal's endianness.
|
|
373
|
+
#
|
|
374
|
+
# @param [Array<Integer>] bytes The message byte array to modify
|
|
375
|
+
# @param [Integer] raw The processed value to write
|
|
376
|
+
# @return [void]
|
|
377
|
+
def write_bits_to_bytes(bytes, raw)
|
|
378
|
+
length.times do |i|
|
|
379
|
+
bit = (raw >> i) & 1
|
|
380
|
+
bit_pos = calculate_bit_position(i)
|
|
381
|
+
byte_index, bit_index = calculate_byte_and_bit_indices(bit_pos)
|
|
382
|
+
|
|
383
|
+
validate_bit_position(bit_pos, bytes.size)
|
|
384
|
+
update_byte_with_bit(bytes, byte_index, bit_index, bit)
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Calculates the bit position for a given bit offset within the signal.
|
|
389
|
+
#
|
|
390
|
+
# Handles both little-endian and big-endian bit ordering according
|
|
391
|
+
# to the signal's endianness setting.
|
|
392
|
+
#
|
|
393
|
+
# @param [Integer] bit_offset The offset within the signal (0 to length-1)
|
|
394
|
+
# @return [Integer] The absolute bit position within the message
|
|
395
|
+
def calculate_bit_position(bit_offset)
|
|
396
|
+
if endianness == :little
|
|
397
|
+
start_bit + bit_offset
|
|
398
|
+
else
|
|
399
|
+
# For big-endian signals, the bit numbering within a byte follows MSB-first
|
|
400
|
+
# ordering. This means that the most significant bit (MSB) is numbered 7,
|
|
401
|
+
# and the least significant bit (LSB) is numbered 0. To calculate the absolute
|
|
402
|
+
# bit position, we first determine the position of the MSB in the starting byte.
|
|
403
|
+
#
|
|
404
|
+
# The formula ((start_bit / 8) * 8) calculates the starting byte's base bit
|
|
405
|
+
# position (aligned to the nearest multiple of 8). Adding (7 - (start_bit % 8))
|
|
406
|
+
# adjusts this base position to point to the MSB of the starting byte.
|
|
407
|
+
#
|
|
408
|
+
# Finally, we subtract the bit offset to account for the signal's length and
|
|
409
|
+
# position within the message.
|
|
410
|
+
base = ((start_bit / 8) * 8) + (7 - (start_bit % 8))
|
|
411
|
+
base - bit_offset
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Calculates byte and bit indices from an absolute bit position.
|
|
416
|
+
#
|
|
417
|
+
# @param [Integer] bit_pos The absolute bit position within the message
|
|
418
|
+
# @return [Array<Integer>] A two-element array [byte_index, bit_index]
|
|
419
|
+
def calculate_byte_and_bit_indices(bit_pos)
|
|
420
|
+
[bit_pos / 8, bit_pos % 8]
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Validates that a bit position is within the message boundaries.
|
|
424
|
+
#
|
|
425
|
+
# @param [Integer] bit_pos The bit position to validate
|
|
426
|
+
# @param [Integer] bytes_size The size of the message in bytes
|
|
427
|
+
# @return [void]
|
|
428
|
+
# @raise [ArgumentError] If the bit position is out of bounds
|
|
429
|
+
def validate_bit_position(bit_pos, bytes_size)
|
|
430
|
+
byte_index = bit_pos / 8
|
|
431
|
+
return unless byte_index >= bytes_size || byte_index.negative?
|
|
432
|
+
|
|
433
|
+
raise ArgumentError, "Bit position #{bit_pos} out of bounds"
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Updates a specific bit within a byte in the message array.
|
|
437
|
+
#
|
|
438
|
+
# Sets or clears the specified bit within the target byte, initializing
|
|
439
|
+
# the byte to 0 if it hasn't been set yet.
|
|
440
|
+
#
|
|
441
|
+
# @param [Array<Integer>] bytes The message byte array to modify
|
|
442
|
+
# @param [Integer] byte_index The index of the byte to modify
|
|
443
|
+
# @param [Integer] bit_index The bit position within the byte (0-7)
|
|
444
|
+
# @param [Integer] bit The bit value to set (0 or 1)
|
|
445
|
+
# @return [void]
|
|
446
|
+
def update_byte_with_bit(bytes, byte_index, bit_index, bit)
|
|
447
|
+
bytes[byte_index] ||= 0
|
|
448
|
+
if bit == 1
|
|
449
|
+
bytes[byte_index] |= (1 << bit_index)
|
|
450
|
+
else
|
|
451
|
+
bytes[byte_index] &= ~(1 << bit_index)
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Extracts the signal value from the message byte array.
|
|
456
|
+
#
|
|
457
|
+
# This is the main decoding method that coordinates bit extraction
|
|
458
|
+
# and sign conversion.
|
|
459
|
+
#
|
|
460
|
+
# @param [Array<Integer>] bytes The message byte array to decode from
|
|
461
|
+
# @return [Integer] The raw integer value extracted from the message
|
|
462
|
+
def extract_bits(bytes)
|
|
463
|
+
value = read_bits_from_bytes(bytes)
|
|
464
|
+
convert_to_signed_if_needed(value)
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# Reads the raw bits from the message byte array.
|
|
468
|
+
#
|
|
469
|
+
# Extracts each bit of the signal from the message bytes, building
|
|
470
|
+
# up the raw integer value bit by bit.
|
|
471
|
+
#
|
|
472
|
+
# @param [Array<Integer>] bytes The message byte array to read from
|
|
473
|
+
# @return [Integer] The raw unsigned integer value
|
|
474
|
+
def read_bits_from_bytes(bytes)
|
|
475
|
+
value = 0
|
|
476
|
+
length.times do |i|
|
|
477
|
+
bit_pos = calculate_bit_position(i)
|
|
478
|
+
byte_index, bit_index = calculate_byte_and_bit_indices(bit_pos)
|
|
479
|
+
|
|
480
|
+
validate_extraction_bounds(bit_pos, bytes.size)
|
|
481
|
+
|
|
482
|
+
bit = ((bytes[byte_index] || 0) >> bit_index) & 1
|
|
483
|
+
value |= (bit << i)
|
|
484
|
+
end
|
|
485
|
+
value
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# Validates bit position during extraction to ensure it's within bounds.
|
|
489
|
+
#
|
|
490
|
+
# @param [Integer] bit_pos The bit position to validate
|
|
491
|
+
# @param [Integer] bytes_size The size of the message in bytes
|
|
492
|
+
# @return [void]
|
|
493
|
+
# @raise [ArgumentError] If the bit position is out of bounds
|
|
494
|
+
def validate_extraction_bounds(bit_pos, bytes_size)
|
|
495
|
+
byte_index = bit_pos / 8
|
|
496
|
+
return unless byte_index >= bytes_size || byte_index.negative?
|
|
497
|
+
|
|
498
|
+
raise ArgumentError, "Bit position #{bit_pos} out of bounds during extraction"
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# Converts unsigned value to signed if the signal is signed and the MSB is set.
|
|
502
|
+
#
|
|
503
|
+
# For signed signals, checks if the most significant bit is set and
|
|
504
|
+
# converts the value from two's complement representation to a negative integer.
|
|
505
|
+
#
|
|
506
|
+
# @param [Integer] value The unsigned integer value to potentially convert
|
|
507
|
+
# @return [Integer] The final signed or unsigned value
|
|
508
|
+
def convert_to_signed_if_needed(value)
|
|
509
|
+
return value unless sign == :signed && length.positive?
|
|
510
|
+
|
|
511
|
+
msb_set = (value >> (length - 1)).allbits?(1)
|
|
512
|
+
msb_set ? value - (1 << length) : value
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "socket"
|
|
4
3
|
require "logger"
|
|
4
|
+
require_relative "adapter/socketcan"
|
|
5
5
|
|
|
6
6
|
module CanMessenger
|
|
7
7
|
# Messenger
|
|
@@ -15,25 +15,24 @@ module CanMessenger
|
|
|
15
15
|
# messenger.start_listening do |message|
|
|
16
16
|
# puts "Received: ID=#{message[:id]}, Data=#{message[:data].map { |b| '0x%02X' % b }}"
|
|
17
17
|
# end
|
|
18
|
-
class Messenger
|
|
19
|
-
FRAME_SIZE = 16
|
|
20
|
-
CANFD_FRAME_SIZE = 72
|
|
21
|
-
MIN_FRAME_SIZE = 8
|
|
22
|
-
MAX_FD_DATA = 64
|
|
23
|
-
TIMEOUT = [1, 0].pack("l_2")
|
|
24
|
-
|
|
18
|
+
class Messenger
|
|
25
19
|
# Initializes a new Messenger instance.
|
|
26
20
|
#
|
|
27
21
|
# @param [String] interface_name The CAN interface to use (e.g., 'can0').
|
|
28
22
|
# @param [Logger, nil] logger Optional logger for error handling and debug information.
|
|
29
|
-
# @param [Symbol] endianness The endianness of the CAN ID (default: :
|
|
23
|
+
# @param [Symbol] endianness The endianness of the CAN ID (default: :native) can be :big, :little, or :native.
|
|
30
24
|
# @return [void]
|
|
31
|
-
def initialize(interface_name:, logger: nil, endianness: :
|
|
25
|
+
def initialize(interface_name:, logger: nil, endianness: :native, can_fd: false, adapter: Adapter::Socketcan)
|
|
32
26
|
@interface_name = interface_name
|
|
33
27
|
@logger = logger || Logger.new($stdout)
|
|
34
28
|
@listening = true # Control flag for listening loop
|
|
35
|
-
@endianness = endianness # :big or :
|
|
29
|
+
@endianness = endianness # :big, :little, or :native
|
|
36
30
|
@can_fd = can_fd
|
|
31
|
+
@adapter = if adapter.is_a?(Class)
|
|
32
|
+
adapter.new(interface_name: interface_name, logger: @logger, endianness: endianness)
|
|
33
|
+
else
|
|
34
|
+
adapter
|
|
35
|
+
end
|
|
37
36
|
end
|
|
38
37
|
|
|
39
38
|
# Sends a CAN message by writing directly to a raw CAN socket
|
|
@@ -42,10 +41,12 @@ module CanMessenger
|
|
|
42
41
|
# @param [Array<Integer>] data The data bytes of the CAN message (0 to 8 bytes).
|
|
43
42
|
# @return [void]
|
|
44
43
|
def send_can_message(id:, data:, extended_id: false, can_fd: nil)
|
|
44
|
+
raise ArgumentError, "id and data are required" if id.nil? || data.nil?
|
|
45
|
+
|
|
45
46
|
use_fd = can_fd.nil? ? @can_fd : can_fd
|
|
46
47
|
|
|
47
48
|
with_socket(can_fd: use_fd) do |socket|
|
|
48
|
-
frame = build_can_frame(id: id, data: data, extended_id: extended_id, can_fd: use_fd)
|
|
49
|
+
frame = @adapter.build_can_frame(id: id, data: data, extended_id: extended_id, can_fd: use_fd)
|
|
49
50
|
socket.write(frame)
|
|
50
51
|
end
|
|
51
52
|
rescue ArgumentError
|
|
@@ -54,6 +55,23 @@ module CanMessenger
|
|
|
54
55
|
@logger.error("Error sending CAN message (ID: #{id}): #{e}")
|
|
55
56
|
end
|
|
56
57
|
|
|
58
|
+
# Encodes and sends a CAN message using a DBC definition
|
|
59
|
+
#
|
|
60
|
+
# @param [String] message_name The message name to encode
|
|
61
|
+
# @param [Hash] signals Values for each signal in the message
|
|
62
|
+
# @param [CanMessenger::DBC] dbc The DBC instance used for encoding (defaults to @dbc)
|
|
63
|
+
# @return [void]
|
|
64
|
+
def send_dbc_message(message_name:, signals:, dbc: @dbc, extended_id: false, can_fd: nil)
|
|
65
|
+
raise ArgumentError, "dbc is required" if dbc.nil?
|
|
66
|
+
|
|
67
|
+
encoded = dbc.encode_can(message_name, signals)
|
|
68
|
+
send_can_message(id: encoded[:id], data: encoded[:data], extended_id: extended_id, can_fd: can_fd)
|
|
69
|
+
rescue ArgumentError
|
|
70
|
+
raise
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
@logger.error("Error sending DBC message #{message_name}: #{e}")
|
|
73
|
+
end
|
|
74
|
+
|
|
57
75
|
# Continuously listens for CAN messages on the specified interface.
|
|
58
76
|
#
|
|
59
77
|
# This method listens for incoming CAN messages and applies an optional filter.
|
|
@@ -67,7 +85,7 @@ module CanMessenger
|
|
|
67
85
|
# - `:id` [Integer] the CAN message ID
|
|
68
86
|
# - `:data` [Array<Integer>] the message data bytes
|
|
69
87
|
# @return [void]
|
|
70
|
-
def start_listening(filter: nil, can_fd: nil, &
|
|
88
|
+
def start_listening(filter: nil, can_fd: nil, dbc: nil, &)
|
|
71
89
|
return @logger.error("No block provided to handle messages.") unless block_given?
|
|
72
90
|
|
|
73
91
|
@listening = true
|
|
@@ -76,7 +94,7 @@ module CanMessenger
|
|
|
76
94
|
|
|
77
95
|
with_socket(can_fd: use_fd) do |socket|
|
|
78
96
|
@logger.info("Started listening on #{@interface_name}")
|
|
79
|
-
process_message(socket, filter, use_fd, &
|
|
97
|
+
process_message(socket, filter, use_fd, dbc, &) while @listening
|
|
80
98
|
end
|
|
81
99
|
end
|
|
82
100
|
|
|
@@ -96,7 +114,7 @@ module CanMessenger
|
|
|
96
114
|
# @yield [socket] An open CAN socket.
|
|
97
115
|
# @return [void]
|
|
98
116
|
def with_socket(can_fd: @can_fd)
|
|
99
|
-
socket =
|
|
117
|
+
socket = @adapter.open_socket(can_fd: can_fd)
|
|
100
118
|
return @logger.error("Failed to open socket, cannot continue operation.") if socket.nil?
|
|
101
119
|
|
|
102
120
|
yield socket
|
|
@@ -104,133 +122,25 @@ module CanMessenger
|
|
|
104
122
|
socket&.close
|
|
105
123
|
end
|
|
106
124
|
|
|
107
|
-
# Creates and configures a CAN socket bound to @interface_name.
|
|
108
|
-
#
|
|
109
|
-
# @return [Socket, nil] The configured CAN socket, or nil if the socket cannot be opened.
|
|
110
|
-
def open_can_socket(can_fd: @can_fd) # rubocop:disable Metrics/MethodLength
|
|
111
|
-
socket = Socket.open(Socket::PF_CAN, Socket::SOCK_RAW, Socket::CAN_RAW)
|
|
112
|
-
socket.bind(Socket.pack_sockaddr_can(@interface_name))
|
|
113
|
-
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, TIMEOUT)
|
|
114
|
-
if can_fd && Socket.const_defined?(:CAN_RAW_FD_FRAMES)
|
|
115
|
-
socket.setsockopt(Socket.const_defined?(:SOL_CAN_RAW) ? Socket::SOL_CAN_RAW : Socket::CAN_RAW,
|
|
116
|
-
Socket::CAN_RAW_FD_FRAMES, 1)
|
|
117
|
-
end
|
|
118
|
-
socket
|
|
119
|
-
rescue StandardError => e
|
|
120
|
-
@logger.error("Error creating CAN socket on interface #{@interface_name}: #{e}")
|
|
121
|
-
nil
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# Builds a raw CAN or CAN FD frame for SocketCAN.
|
|
125
|
-
#
|
|
126
|
-
# @param id [Integer] the CAN ID
|
|
127
|
-
# @param data [Array<Integer>] data bytes (up to 8 for classic, 64 for CAN FD)
|
|
128
|
-
# @param can_fd [Boolean] whether to build a CAN FD frame
|
|
129
|
-
# @return [String] the packed CAN frame
|
|
130
|
-
def build_can_frame(id:, data:, extended_id: false, can_fd: false) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
131
|
-
if can_fd
|
|
132
|
-
raise ArgumentError, "CAN FD data cannot exceed #{MAX_FD_DATA} bytes" if data.size > MAX_FD_DATA
|
|
133
|
-
elsif data.size > 8
|
|
134
|
-
raise ArgumentError, "CAN data cannot exceed 8 bytes"
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# Mask the ID to 29 bits
|
|
138
|
-
can_id = id & 0x1FFFFFFF
|
|
139
|
-
|
|
140
|
-
# If extended_id == true, set bit 31 (CAN_EFF_FLAG)
|
|
141
|
-
can_id |= 0x80000000 if extended_id
|
|
142
|
-
|
|
143
|
-
# Pack the 4‐byte ID (big-endian or little-endian)
|
|
144
|
-
id_bytes = @endianness == :big ? [can_id].pack("L>") : [can_id].pack("V")
|
|
145
|
-
|
|
146
|
-
# 1 byte for DLC/length, then 3 bytes for flags/reserved
|
|
147
|
-
dlc_and_pad = [data.size, 0, 0, 0].pack("C*")
|
|
148
|
-
|
|
149
|
-
payload = if can_fd
|
|
150
|
-
data.pack("C*").ljust(MAX_FD_DATA, "\x00")
|
|
151
|
-
else
|
|
152
|
-
data.pack("C*").ljust(8, "\x00")
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
id_bytes + dlc_and_pad + payload
|
|
156
|
-
end
|
|
157
|
-
|
|
158
125
|
# Processes a single CAN message from `socket`. Applies filter, yields to block if it matches.
|
|
159
126
|
#
|
|
160
127
|
# @param socket [Socket] The CAN socket.
|
|
161
128
|
# @param filter [Integer, Range, Array<Integer>, nil] Optional filter for CAN IDs.
|
|
162
129
|
# @yield [message] Yields the message if it passes filtering.
|
|
163
130
|
# @return [void]
|
|
164
|
-
def process_message(socket, filter, can_fd)
|
|
165
|
-
message = receive_message(socket: socket, can_fd: can_fd)
|
|
131
|
+
def process_message(socket, filter, can_fd, dbc, &block)
|
|
132
|
+
message = @adapter.receive_message(socket: socket, can_fd: can_fd)
|
|
166
133
|
return if message.nil?
|
|
167
134
|
return if filter && !matches_filter?(message_id: message[:id], filter: filter)
|
|
168
135
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
# Reads a frame from the socket and parses it into { id:, data: }, or nil if none is received.
|
|
175
|
-
#
|
|
176
|
-
# @param socket [Socket]
|
|
177
|
-
# @return [Hash, nil]
|
|
178
|
-
def receive_message(socket:, can_fd: false)
|
|
179
|
-
frame_size = can_fd ? CANFD_FRAME_SIZE : FRAME_SIZE
|
|
180
|
-
frame = socket.recv(frame_size)
|
|
181
|
-
return nil if frame.nil? || frame.size < MIN_FRAME_SIZE
|
|
182
|
-
|
|
183
|
-
parse_frame(frame: frame, can_fd: can_fd)
|
|
184
|
-
rescue IO::WaitReadable
|
|
185
|
-
nil
|
|
186
|
-
rescue StandardError => e
|
|
187
|
-
@logger.error("Error receiving CAN message on interface #{@interface_name}: #{e}")
|
|
188
|
-
nil
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
# Parses a raw CAN frame into { id: Integer, data: Array<Integer> }, or nil on error.
|
|
192
|
-
#
|
|
193
|
-
# @param [String] frame
|
|
194
|
-
# @return [Hash, nil]
|
|
195
|
-
def parse_frame(frame:, can_fd: nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
196
|
-
return nil unless frame && frame.size >= MIN_FRAME_SIZE
|
|
197
|
-
|
|
198
|
-
use_fd = can_fd.nil? ? frame.size >= CANFD_FRAME_SIZE : can_fd
|
|
199
|
-
|
|
200
|
-
raw_id = unpack_frame_id(frame: frame)
|
|
201
|
-
|
|
202
|
-
# Determine if EFF bit is set
|
|
203
|
-
extended = raw_id.anybits?(0x80000000)
|
|
204
|
-
# or raw_id.anybits?(0x80000000) if your Ruby version supports `Integer#anybits?`
|
|
205
|
-
|
|
206
|
-
# Now mask off everything except the lower 29 bits
|
|
207
|
-
id = raw_id & 0x1FFFFFFF
|
|
208
|
-
|
|
209
|
-
data_length = if use_fd
|
|
210
|
-
frame[4].ord
|
|
211
|
-
else
|
|
212
|
-
frame[4].ord & 0x0F
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
# Extract data
|
|
216
|
-
data = if frame.size >= MIN_FRAME_SIZE + data_length
|
|
217
|
-
frame[MIN_FRAME_SIZE, data_length].unpack("C*")
|
|
218
|
-
else
|
|
219
|
-
[]
|
|
220
|
-
end
|
|
136
|
+
if dbc
|
|
137
|
+
decoded = dbc.decode_can(message[:id], message[:data])
|
|
138
|
+
message[:decoded] = decoded if decoded
|
|
139
|
+
end
|
|
221
140
|
|
|
222
|
-
|
|
141
|
+
block.call(message)
|
|
223
142
|
rescue StandardError => e
|
|
224
|
-
@logger.error("
|
|
225
|
-
nil
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
def unpack_frame_id(frame:)
|
|
229
|
-
if @endianness == :big
|
|
230
|
-
frame[0..3].unpack1("L>")
|
|
231
|
-
else
|
|
232
|
-
frame[0..3].unpack1("V")
|
|
233
|
-
end
|
|
143
|
+
@logger.error("Unexpected error in listening loop: #{e.message}")
|
|
234
144
|
end
|
|
235
145
|
|
|
236
146
|
# Checks whether the given message ID matches the specified filter.
|
data/lib/can_messenger.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: can_messenger
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- fk1018
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies: []
|
|
13
12
|
description: CanMessenger provides an interface to send and receive messages over
|
|
14
13
|
the CAN bus, useful for applications requiring CAN communication in Ruby.
|
|
@@ -18,8 +17,13 @@ executables: []
|
|
|
18
17
|
extensions: []
|
|
19
18
|
extra_rdoc_files: []
|
|
20
19
|
files:
|
|
20
|
+
- CHANGELOG.md
|
|
21
|
+
- LICENSE.txt
|
|
21
22
|
- README.md
|
|
22
23
|
- lib/can_messenger.rb
|
|
24
|
+
- lib/can_messenger/adapter/base.rb
|
|
25
|
+
- lib/can_messenger/adapter/socketcan.rb
|
|
26
|
+
- lib/can_messenger/dbc.rb
|
|
23
27
|
- lib/can_messenger/messenger.rb
|
|
24
28
|
- lib/can_messenger/version.rb
|
|
25
29
|
homepage: https://github.com/fk1018/can_messenger
|
|
@@ -31,7 +35,6 @@ metadata:
|
|
|
31
35
|
source_code_uri: https://github.com/fk1018/can_messenger
|
|
32
36
|
changelog_uri: https://github.com/fk1018/can_messenger/blob/main/CHANGELOG.md
|
|
33
37
|
rubygems_mfa_required: 'true'
|
|
34
|
-
post_install_message:
|
|
35
38
|
rdoc_options: []
|
|
36
39
|
require_paths:
|
|
37
40
|
- lib
|
|
@@ -39,15 +42,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
39
42
|
requirements:
|
|
40
43
|
- - ">="
|
|
41
44
|
- !ruby/object:Gem::Version
|
|
42
|
-
version:
|
|
45
|
+
version: 4.0.1
|
|
43
46
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
44
47
|
requirements:
|
|
45
48
|
- - ">="
|
|
46
49
|
- !ruby/object:Gem::Version
|
|
47
50
|
version: '0'
|
|
48
51
|
requirements: []
|
|
49
|
-
rubygems_version:
|
|
50
|
-
signing_key:
|
|
52
|
+
rubygems_version: 4.0.3
|
|
51
53
|
specification_version: 4
|
|
52
54
|
summary: A simple Ruby wrapper to read and write CAN bus messages.
|
|
53
55
|
test_files: []
|