can_messenger 2.3.1 → 2.3.2
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 +22 -1
- data/README.md +12 -4
- data/lib/can_messenger/adapter/base.rb +65 -7
- data/lib/can_messenger/adapter/socketcan.rb +131 -10
- data/lib/can_messenger/constants.rb +10 -0
- data/lib/can_messenger/dbc.rb +133 -30
- data/lib/can_messenger/messenger.rb +3 -0
- data/lib/can_messenger/version.rb +2 -1
- data/lib/can_messenger.rb +3 -0
- data/sig/can_messenger/adapter/base.rbs +42 -0
- data/sig/can_messenger/adapter/socketcan.rbs +49 -0
- data/sig/can_messenger/constants.rbs +8 -0
- data/sig/can_messenger/dbc.rbs +158 -0
- data/sig/can_messenger/messenger.rbs +78 -0
- data/sig/can_messenger/version.rbs +3 -0
- data/sig/can_messenger.rbs +4 -0
- metadata +25 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 146ac35cbdc1ef83f90ebc6fb0e41385d2c1787cff145aa8ca346ceb606ed9da
|
|
4
|
+
data.tar.gz: b09ee62d02ddd427688c3fa8e33b39ee633108e504b3675386d3d01f30a02f79
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1072360ce734343ffe92e47c47524f19ca9975705b3d12667e95d1d7ddff980e9ffbcfce630e05c10eacfa07b56dbabd4da0f801cb474d0d639e446f38f8c59e
|
|
7
|
+
data.tar.gz: d1c4ed455124f0fcf8a18985ad111daa3fe429fe688dc40584a31be439afeb34c5f6a32fb91740e03eaa2f8dbccc83eae0fe14aba18b80ad425d37971f91e643
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [2.3.2] - 2026-10-30
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **Breaking:** Require Ruby 4.0.9 or higher and update the Ruby development and CI images.
|
|
8
|
+
- Update the docs toolchain to Node.js 24.22.1 and Docusaurus 3.10.2, refresh locked dependencies, and override vulnerable transitive dependencies with patched versions.
|
|
9
|
+
- Update the locked Ruby JSON dependency to address published security advisories.
|
|
10
|
+
- DBC signals with integral scaling now decode to `Integer` values to preserve precision; fractional scaling continues to return `Float` values.
|
|
11
|
+
- Promote the docs site stable snapshot to `2.3.2`.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Let the SocketCAN listener stop on an idle bus by bounding receive waits.
|
|
16
|
+
- Preserve remote transmission requests as distinct frames and skip DBC decoding for them.
|
|
17
|
+
- Reject CAN payload elements outside the integer byte range instead of silently wrapping them.
|
|
18
|
+
- Preserve large integer DBC values and round correctly when directly constructed signals use integer scaling factors.
|
|
19
|
+
- Reject malformed DBC scaling, invalid byte order, unsupported metadata directives, and unsupported floating-point signal types with line context.
|
|
20
|
+
- Declare Logger as a runtime dependency so minimal Bundler applications can load the gem.
|
|
21
|
+
- Ship the RBS signatures in the gem and load them explicitly during signature validation.
|
|
22
|
+
|
|
3
23
|
## [2.3.1] - 2026-04-15
|
|
4
24
|
|
|
5
25
|
### Changed
|
|
@@ -200,7 +220,8 @@
|
|
|
200
220
|
## [0.1.0] - 2024-11-10
|
|
201
221
|
|
|
202
222
|
- Initial release
|
|
203
|
-
[Unreleased]: https://github.com/fk1018/can_messenger/compare/v2.3.
|
|
223
|
+
[Unreleased]: https://github.com/fk1018/can_messenger/compare/v2.3.2...HEAD
|
|
224
|
+
[2.3.2]: https://github.com/fk1018/can_messenger/compare/v2.3.1...v2.3.2
|
|
204
225
|
[2.3.1]: https://github.com/fk1018/can_messenger/compare/v2.3.0...v2.3.1
|
|
205
226
|
[2.3.0]: https://github.com/fk1018/can_messenger/compare/v2.2.0...v2.3.0
|
|
206
227
|
[2.2.0]: https://github.com/fk1018/can_messenger/compare/v2.1.0...v2.2.0
|
data/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
## Requirements
|
|
14
14
|
|
|
15
|
-
- Ruby 4.0.
|
|
15
|
+
- Ruby 4.0.9 or higher.
|
|
16
16
|
- Docker (optional, for containerized development without installing Ruby locally).
|
|
17
17
|
|
|
18
18
|
## Installation
|
|
@@ -57,6 +57,8 @@ To send a message:
|
|
|
57
57
|
messenger.send_can_message(id: 0x123, data: [0xDE, 0xAD, 0xBE, 0xEF])
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
Each payload element must be an integer in `0..255`; invalid bytes raise `ArgumentError`.
|
|
61
|
+
|
|
60
62
|
> **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.
|
|
61
63
|
> The SocketCAN adapter builds the Linux socket address itself, so it does not depend on Ruby exposing `CAN_RAW` or `pack_sockaddr_can`.
|
|
62
64
|
|
|
@@ -85,6 +87,8 @@ messenger.start_listening do |msg|
|
|
|
85
87
|
end
|
|
86
88
|
```
|
|
87
89
|
|
|
90
|
+
Remote transmission request (RTR) frames include `remote: true`, the requested byte count in `dlc`, and an empty `data` array. They are yielded without DBC decoding because they contain no signal data.
|
|
91
|
+
|
|
88
92
|
#### Listening with Filters
|
|
89
93
|
|
|
90
94
|
The `start_listening` method supports filtering incoming messages based on CAN ID:
|
|
@@ -138,7 +142,9 @@ end
|
|
|
138
142
|
If the DBC message definition uses an extended CAN ID, `send_dbc_message` automatically sends it as an extended frame and `start_listening(dbc: ...)` decodes received extended frames back through the same DBC definition.
|
|
139
143
|
If DBC decoding raises because the payload does not match the signal definition, the raw frame is still yielded and includes `decode_error: { class:, message: }`.
|
|
140
144
|
|
|
141
|
-
Malformed DBC files
|
|
145
|
+
Malformed DBC files fail fast with `ArgumentError` that includes the line number and offending line text. Signal byte order must be `@0` or `@1`, and scaling values must be valid numeric literals. Standard metadata directives such as `VERSION`, `NS_`, `BS_`, `BU_`, `CM_`, `VAL_`, `BA_`, `BA_DEF_`, `BA_DEF_DEF_`, and `BO_TX_BU_` are ignored. `SIG_VALTYPE_` accepts integer type `0`; floating-point types `1` and `2` are unsupported and raise explicitly. Other non-empty directives, including unsupported `BA_*` variants, raise with line context.
|
|
146
|
+
|
|
147
|
+
Identity-scaled integer signals preserve all 64 bits during encoding and decoding. Integral scaling returns `Integer` values; fractional scaling returns `Float` values. Directly constructed signals also round correctly with integer scaling factors.
|
|
142
148
|
|
|
143
149
|
### Stopping the Listener
|
|
144
150
|
|
|
@@ -148,6 +154,8 @@ To stop listening, use:
|
|
|
148
154
|
messenger.stop_listening
|
|
149
155
|
```
|
|
150
156
|
|
|
157
|
+
The default SocketCAN adapter checks for shutdown at least once per second while the bus is idle. When listening in another thread, join that thread after stopping to wait for socket cleanup.
|
|
158
|
+
|
|
151
159
|
### Adapters
|
|
152
160
|
|
|
153
161
|
`CanMessenger::Messenger` delegates low-level CAN bus operations to an adapter. By default it uses the
|
|
@@ -199,7 +207,7 @@ Before using `can_messenger`, please note the following:
|
|
|
199
207
|
```
|
|
200
208
|
|
|
201
209
|
- **Threading & Socket Management:**
|
|
202
|
-
- **Blocking Behavior:** The
|
|
210
|
+
- **Blocking Behavior:** `start_listening` runs until stopped. The default SocketCAN adapter bounds each receive wait to one second so shutdown also works on an idle bus. Always call `stop_listening` to gracefully shut down the listener; custom adapters should also bound their receive waits.
|
|
203
211
|
- **Resource Cleanup:** The socket is automatically closed when the listening loop terminates. Stop the listener to avoid resource leaks.
|
|
204
212
|
|
|
205
213
|
- **Logging:**
|
|
@@ -278,7 +286,7 @@ If you already have Ruby installed locally, you can still use:
|
|
|
278
286
|
|
|
279
287
|
```bash
|
|
280
288
|
bin/setup
|
|
281
|
-
rbs validate
|
|
289
|
+
rbs -I sig -r logger validate
|
|
282
290
|
bundle exec rake test:rspec
|
|
283
291
|
bundle exec rubocop
|
|
284
292
|
```
|
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
module CanMessenger
|
|
4
4
|
module Adapter
|
|
5
|
-
#
|
|
6
|
-
#
|
|
5
|
+
# Defines the transport interface used by Messenger.
|
|
6
|
+
#
|
|
7
|
+
# Provides shared interface, logging, and CAN ID byte-order configuration.
|
|
8
|
+
# Concrete adapters implement socket creation, frame building, receiving,
|
|
9
|
+
# and parsing for their transport.
|
|
10
|
+
#
|
|
11
|
+
# @abstract Subclass and implement the four transport methods below.
|
|
7
12
|
class Base
|
|
8
13
|
attr_reader :interface_name, :logger, :endianness
|
|
9
14
|
|
|
15
|
+
# Detects the host's byte order by comparing native and explicit integer packing.
|
|
16
|
+
#
|
|
17
|
+
# @return [Symbol] :little or :big
|
|
10
18
|
def self.native_endianness
|
|
11
19
|
native = pack_uint(1, "I")
|
|
12
20
|
return :little if native == pack_uint(1, "V")
|
|
@@ -16,11 +24,26 @@ module CanMessenger
|
|
|
16
24
|
native.bytes.first == 1 ? :little : :big
|
|
17
25
|
end
|
|
18
26
|
|
|
27
|
+
# Packs an unsigned integer for byte-order detection.
|
|
28
|
+
#
|
|
29
|
+
# @param [Integer] value The integer to pack
|
|
30
|
+
# @param [String] template The Array#pack format specifying width and byte order
|
|
31
|
+
# @return [String] The packed bytes
|
|
19
32
|
def self.pack_uint(value, template)
|
|
20
33
|
[value].pack(template)
|
|
21
34
|
end
|
|
22
35
|
private_class_method :pack_uint
|
|
23
36
|
|
|
37
|
+
# Initializes the shared adapter configuration without opening a socket.
|
|
38
|
+
#
|
|
39
|
+
# Resolves :native to the host's byte order so transport methods only need
|
|
40
|
+
# to handle :little and :big.
|
|
41
|
+
#
|
|
42
|
+
# @param [String] interface_name The CAN interface to use (e.g., "can0")
|
|
43
|
+
# @param [Logger] logger The logger for transport errors and diagnostic messages
|
|
44
|
+
# @param [Symbol, String] endianness CAN ID byte order: native, little, or big
|
|
45
|
+
# @return [void]
|
|
46
|
+
# @raise [ArgumentError] If the byte-order setting is unsupported
|
|
24
47
|
def initialize(interface_name:, logger:, endianness: :native)
|
|
25
48
|
@interface_name = interface_name
|
|
26
49
|
@logger = logger
|
|
@@ -28,29 +51,64 @@ module CanMessenger
|
|
|
28
51
|
@endianness = normalized_endianness == :native ? self.class.native_endianness : normalized_endianness
|
|
29
52
|
end
|
|
30
53
|
|
|
31
|
-
#
|
|
32
|
-
#
|
|
54
|
+
# Opens a socket for the underlying interface.
|
|
55
|
+
#
|
|
56
|
+
# Messenger owns the returned socket and closes it after sending or listening.
|
|
57
|
+
#
|
|
58
|
+
# @param [Boolean, nil] can_fd Whether to enable CAN FD frames
|
|
59
|
+
# @return [Object, nil] The adapter-specific socket, or nil if opening fails
|
|
60
|
+
# @raise [NotImplementedError] If a subclass does not implement this method
|
|
33
61
|
def open_socket(can_fd: false)
|
|
34
62
|
raise NotImplementedError, "open_socket must be implemented in subclasses"
|
|
35
63
|
end
|
|
36
64
|
|
|
37
|
-
#
|
|
65
|
+
# Builds a frame ready to be written to the socket.
|
|
66
|
+
#
|
|
67
|
+
# @param [Integer] id The CAN identifier without frame flags
|
|
68
|
+
# @param [Array<Integer>] data The payload bytes, each in 0..255
|
|
69
|
+
# @param [Boolean] extended_id Whether to use a 29-bit identifier instead of an 11-bit identifier
|
|
70
|
+
# @param [Boolean, nil] can_fd Whether to build a CAN FD frame
|
|
71
|
+
# @return [String] The serialized frame
|
|
72
|
+
# @raise [NotImplementedError] If a subclass does not implement this method
|
|
38
73
|
def build_can_frame(id:, data:, extended_id: false, can_fd: false)
|
|
39
74
|
raise NotImplementedError, "build_can_frame must be implemented in subclasses"
|
|
40
75
|
end
|
|
41
76
|
|
|
42
|
-
#
|
|
77
|
+
# Receives and parses a frame from the socket.
|
|
78
|
+
#
|
|
79
|
+
# Implementations should bound their receive wait and return nil on timeout
|
|
80
|
+
# so Messenger can check whether listening has been stopped.
|
|
81
|
+
#
|
|
82
|
+
# @param [Object] socket The adapter-specific socket to read from
|
|
83
|
+
# @param [Boolean, nil] can_fd Whether CAN FD reception is enabled
|
|
84
|
+
# @return [Hash, nil] A message with :id, :data, and :extended, or nil when no message is available
|
|
85
|
+
# @raise [NotImplementedError] If a subclass does not implement this method
|
|
43
86
|
def receive_message(socket:, can_fd: false)
|
|
44
87
|
raise NotImplementedError, "receive_message must be implemented in subclasses"
|
|
45
88
|
end
|
|
46
89
|
|
|
47
|
-
#
|
|
90
|
+
# Parses a raw frame string into a message hash.
|
|
91
|
+
#
|
|
92
|
+
# Remote transmission requests should include :remote and :dlc, with empty
|
|
93
|
+
# :data, so Messenger can distinguish them from frames containing signals.
|
|
94
|
+
#
|
|
95
|
+
# @param [String] frame The serialized frame
|
|
96
|
+
# @param [Boolean, nil] can_fd Whether to parse CAN FD, or nil for adapter-specific detection
|
|
97
|
+
# @return [Hash, nil] A message with :id, :data, and :extended, or nil if parsing fails
|
|
98
|
+
# @raise [NotImplementedError] If a subclass does not implement this method
|
|
48
99
|
def parse_frame(frame:, can_fd: false)
|
|
49
100
|
raise NotImplementedError, "parse_frame must be implemented in subclasses"
|
|
50
101
|
end
|
|
51
102
|
|
|
52
103
|
private
|
|
53
104
|
|
|
105
|
+
# Converts a supported byte-order setting to a symbol.
|
|
106
|
+
#
|
|
107
|
+
# String settings are case-insensitive and may contain surrounding whitespace.
|
|
108
|
+
#
|
|
109
|
+
# @param [Symbol, String] endianness The requested CAN ID byte order
|
|
110
|
+
# @return [Symbol] :native, :little, or :big
|
|
111
|
+
# @raise [ArgumentError] If the setting does not name a supported byte order
|
|
54
112
|
def normalize_endianness(endianness)
|
|
55
113
|
normalized = endianness.is_a?(String) ? endianness.strip.downcase.to_sym : endianness
|
|
56
114
|
return normalized if %i[native little big].include?(normalized)
|
|
@@ -1,29 +1,52 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "socket"
|
|
4
|
+
require "io/wait"
|
|
4
5
|
require_relative "base"
|
|
5
6
|
require_relative "../constants"
|
|
6
7
|
|
|
7
8
|
module CanMessenger
|
|
8
9
|
module Adapter
|
|
9
|
-
#
|
|
10
|
+
# Sends and receives frames through Linux SocketCAN raw sockets.
|
|
11
|
+
#
|
|
12
|
+
# Supports classic CAN and CAN FD, with CAN ID byte order resolved by Base.
|
|
13
|
+
# Frame building and parsing can also be used without opening an interface.
|
|
14
|
+
#
|
|
15
|
+
# @example Building a classic CAN frame
|
|
16
|
+
# adapter = CanMessenger::Adapter::Socketcan.new(interface_name: "can0", logger: Logger.new($stdout))
|
|
17
|
+
# frame = adapter.build_can_frame(id: 0x123, data: [0xDE, 0xAD])
|
|
18
|
+
# frame.bytesize # => 16
|
|
10
19
|
class Socketcan < Base # rubocop:disable Metrics/ClassLength
|
|
20
|
+
# Linux protocol and socket-option identifiers for raw CAN and CAN FD support.
|
|
11
21
|
CAN_RAW = 1
|
|
12
22
|
SOL_CAN_BASE = 100
|
|
13
23
|
SOL_CAN_RAW = SOL_CAN_BASE + CAN_RAW
|
|
14
24
|
CAN_RAW_FD_FRAMES = 5
|
|
25
|
+
|
|
26
|
+
# Complete frame sizes include the header and the fixed-size payload area.
|
|
15
27
|
FRAME_SIZE = 16
|
|
16
28
|
CANFD_FRAME_SIZE = 72
|
|
29
|
+
# Both frame layouts place payload bytes after an eight-byte header.
|
|
17
30
|
MIN_FRAME_SIZE = 8
|
|
18
31
|
MAX_FD_DATA = 64
|
|
19
32
|
MAX_STANDARD_ID = 0x7FF
|
|
33
|
+
|
|
34
|
+
# sockaddr_can has a native family, alignment padding, interface index, and address union.
|
|
20
35
|
SOCKADDR_CAN_ADDR_SIZE = 16
|
|
21
36
|
SOCKADDR_CAN_SIZE = 24
|
|
22
37
|
SOCKADDR_CAN_PADDING = 2
|
|
38
|
+
# Kernel receive timeout as a native timeval: seconds followed by microseconds.
|
|
23
39
|
TIMEOUT = [1, 0].pack("l_2")
|
|
40
|
+
# Raw CAN does not use the protocol-specific address union.
|
|
24
41
|
ZERO_CAN_ADDR = "\x00" * SOCKADDR_CAN_ADDR_SIZE
|
|
25
42
|
|
|
26
43
|
# Creates and configures a CAN socket bound to the interface.
|
|
44
|
+
#
|
|
45
|
+
# Socket creation or configuration errors are logged. Any partially opened
|
|
46
|
+
# socket is closed before returning nil.
|
|
47
|
+
#
|
|
48
|
+
# @param [Boolean, nil] can_fd Whether to enable CAN FD reception and transmission
|
|
49
|
+
# @return [Socket, nil] The bound socket, or nil if it could not be opened
|
|
27
50
|
def open_socket(can_fd: nil)
|
|
28
51
|
socket = Socket.open(Socket::PF_CAN, Socket::SOCK_RAW, CAN_RAW)
|
|
29
52
|
configure_socket(socket, can_fd: can_fd)
|
|
@@ -35,7 +58,19 @@ module CanMessenger
|
|
|
35
58
|
end
|
|
36
59
|
|
|
37
60
|
# Builds a raw CAN or CAN FD frame for SocketCAN.
|
|
61
|
+
#
|
|
62
|
+
# Sets the extended-frame flag when requested and zero-pads the payload area.
|
|
63
|
+
# The header records the supplied byte count, excluding that padding.
|
|
64
|
+
#
|
|
65
|
+
# @param [Integer] id The CAN identifier without frame flags
|
|
66
|
+
# @param [Array<Integer>] data Payload bytes in 0..255; at most 8 for classic CAN or 64 for CAN FD
|
|
67
|
+
# @param [Boolean] extended_id Whether to use a 29-bit identifier instead of an 11-bit identifier
|
|
68
|
+
# @param [Boolean, nil] can_fd Whether to build a CAN FD frame; nil selects classic CAN
|
|
69
|
+
# @return [String] A 16-byte classic CAN frame or a 72-byte CAN FD frame
|
|
70
|
+
# @raise [ArgumentError] If the identifier, payload type, byte values, or payload length are invalid
|
|
38
71
|
def build_can_frame(id:, data:, extended_id: false, can_fd: nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
72
|
+
validate_data!(data)
|
|
73
|
+
|
|
39
74
|
if can_fd
|
|
40
75
|
raise ArgumentError, "CAN FD data cannot exceed #{MAX_FD_DATA} bytes" if data.size > MAX_FD_DATA
|
|
41
76
|
elsif data.size > 8
|
|
@@ -63,9 +98,19 @@ module CanMessenger
|
|
|
63
98
|
end
|
|
64
99
|
|
|
65
100
|
# Reads a frame from the socket and parses it into a hash.
|
|
101
|
+
#
|
|
102
|
+
# Uses a one-second readiness timeout followed by a nonblocking receive so
|
|
103
|
+
# an idle listener can stop. A timeout or loss of readiness returns nil;
|
|
104
|
+
# other receive errors are logged and also return nil.
|
|
105
|
+
#
|
|
106
|
+
# @param [Socket] socket The bound CAN socket to read from
|
|
107
|
+
# @param [Boolean, nil] can_fd Whether to reserve space for CAN FD; nil selects classic CAN
|
|
108
|
+
# @return [Hash, nil] The parsed message, or nil when no frame can be returned
|
|
66
109
|
def receive_message(socket:, can_fd: nil)
|
|
110
|
+
return nil unless socket.wait_readable(1)
|
|
111
|
+
|
|
67
112
|
frame_size = can_fd ? CANFD_FRAME_SIZE : FRAME_SIZE
|
|
68
|
-
frame = socket.
|
|
113
|
+
frame = socket.recv_nonblock(frame_size)
|
|
69
114
|
return nil if frame.nil? || frame.size < MIN_FRAME_SIZE
|
|
70
115
|
|
|
71
116
|
parse_frame(frame: frame, can_fd: can_fd)
|
|
@@ -77,20 +122,27 @@ module CanMessenger
|
|
|
77
122
|
end
|
|
78
123
|
|
|
79
124
|
# Parses a raw CAN frame string into a hash with id, data and extended flag.
|
|
80
|
-
|
|
125
|
+
#
|
|
126
|
+
# The :id excludes frame flags. Remote requests also include :remote => true
|
|
127
|
+
# and the requested byte count in :dlc, with empty :data. If the header is
|
|
128
|
+
# present but the declared payload is incomplete, :data is empty.
|
|
129
|
+
# Parsing errors are logged and return nil.
|
|
130
|
+
#
|
|
131
|
+
# @param [String] frame The raw frame bytes, including the header
|
|
132
|
+
# @param [Boolean, nil] can_fd The frame format, or nil to infer it from the frame size
|
|
133
|
+
# @return [Hash, nil] The message, or nil if the header is missing or parsing fails
|
|
134
|
+
def parse_frame(frame:, can_fd: nil) # rubocop:disable Metrics/MethodLength
|
|
81
135
|
return nil unless frame && frame.size >= MIN_FRAME_SIZE
|
|
82
136
|
|
|
83
|
-
use_fd = can_fd.nil? ? frame.size >= CANFD_FRAME_SIZE : can_fd
|
|
84
|
-
|
|
85
137
|
raw_id = unpack_frame_id(frame: frame)
|
|
86
138
|
extended = raw_id.anybits?(Constants::EXTENDED_ID_FLAG)
|
|
87
139
|
id = raw_id & Constants::MAX_EXTENDED_ID
|
|
88
140
|
|
|
89
|
-
data_length =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
141
|
+
data_length = frame_data_length(frame, can_fd: can_fd)
|
|
142
|
+
|
|
143
|
+
if raw_id.anybits?(Constants::REMOTE_TRANSMISSION_REQUEST_FLAG)
|
|
144
|
+
return { id: id, data: [], extended: extended, remote: true, dlc: data_length }
|
|
145
|
+
end
|
|
94
146
|
|
|
95
147
|
data = if frame.size >= MIN_FRAME_SIZE + data_length
|
|
96
148
|
frame[MIN_FRAME_SIZE, data_length].unpack("C*")
|
|
@@ -106,6 +158,20 @@ module CanMessenger
|
|
|
106
158
|
|
|
107
159
|
private
|
|
108
160
|
|
|
161
|
+
# Reads the declared payload length using the selected frame layout.
|
|
162
|
+
#
|
|
163
|
+
# @param [String] frame A frame containing at least the complete header
|
|
164
|
+
# @param [Boolean, nil] can_fd Whether to use CAN FD, or nil to infer the format from size
|
|
165
|
+
# @return [Integer] The declared number of payload bytes
|
|
166
|
+
def frame_data_length(frame, can_fd:)
|
|
167
|
+
use_fd = can_fd.nil? ? frame.size >= CANFD_FRAME_SIZE : can_fd
|
|
168
|
+
use_fd ? frame[4].ord : frame[4].ord & 0x0F
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Reads the CAN identifier and frame flags using the configured byte order.
|
|
172
|
+
#
|
|
173
|
+
# @param [String] frame The raw frame containing the four-byte identifier field
|
|
174
|
+
# @return [Integer] The identifier with any frame flags still set
|
|
109
175
|
def unpack_frame_id(frame:)
|
|
110
176
|
if endianness == :big
|
|
111
177
|
frame[0..3].unpack1("L>")
|
|
@@ -114,6 +180,16 @@ module CanMessenger
|
|
|
114
180
|
end
|
|
115
181
|
end
|
|
116
182
|
|
|
183
|
+
# Binds a socket, sets its kernel receive timeout, and optionally enables CAN FD.
|
|
184
|
+
#
|
|
185
|
+
# receive_message also bounds its readiness wait because the kernel timeout
|
|
186
|
+
# alone does not limit Ruby's wait for a readable socket.
|
|
187
|
+
#
|
|
188
|
+
# @param [Socket] socket The newly opened CAN socket
|
|
189
|
+
# @param [Boolean, nil] can_fd Whether to enable CAN FD frames
|
|
190
|
+
# @return [void]
|
|
191
|
+
# @raise [ArgumentError] If the configured interface cannot be resolved
|
|
192
|
+
# @raise [SystemCallError] If binding or setting a socket option fails
|
|
117
193
|
def configure_socket(socket, can_fd:)
|
|
118
194
|
socket.bind(build_sockaddr_can(interface_index))
|
|
119
195
|
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, TIMEOUT)
|
|
@@ -122,6 +198,10 @@ module CanMessenger
|
|
|
122
198
|
socket.setsockopt(SOL_CAN_RAW, CAN_RAW_FD_FRAMES, 1)
|
|
123
199
|
end
|
|
124
200
|
|
|
201
|
+
# Closes a socket during failed setup without masking the original error.
|
|
202
|
+
#
|
|
203
|
+
# @param [Socket, nil] socket The socket to close, if one was created
|
|
204
|
+
# @return [void]
|
|
125
205
|
def close_socket(socket)
|
|
126
206
|
return unless socket
|
|
127
207
|
return if socket.respond_to?(:closed?) && socket.closed?
|
|
@@ -131,6 +211,12 @@ module CanMessenger
|
|
|
131
211
|
# Ignore close errors so we can report the original failure.
|
|
132
212
|
end
|
|
133
213
|
|
|
214
|
+
# Resolves the configured interface name to its Linux interface index.
|
|
215
|
+
#
|
|
216
|
+
# Uses Ruby's interface list first, then falls back to the sysfs entry.
|
|
217
|
+
#
|
|
218
|
+
# @return [Integer] The positive interface index used for binding
|
|
219
|
+
# @raise [ArgumentError] If neither lookup can resolve the interface
|
|
134
220
|
def interface_index
|
|
135
221
|
index = interface_index_from_ifaddrs || interface_index_from_sysfs
|
|
136
222
|
return index if index
|
|
@@ -138,6 +224,9 @@ module CanMessenger
|
|
|
138
224
|
raise ArgumentError, "Unknown CAN interface #{interface_name}"
|
|
139
225
|
end
|
|
140
226
|
|
|
227
|
+
# Looks up the interface index through Ruby's network-interface enumeration.
|
|
228
|
+
#
|
|
229
|
+
# @return [Integer, nil] The index, or nil if unavailable, unsupported, or not found
|
|
141
230
|
def interface_index_from_ifaddrs
|
|
142
231
|
return nil unless Socket.respond_to?(:getifaddrs)
|
|
143
232
|
|
|
@@ -153,6 +242,9 @@ module CanMessenger
|
|
|
153
242
|
nil
|
|
154
243
|
end
|
|
155
244
|
|
|
245
|
+
# Reads the Linux sysfs interface index as a fallback for Ruby's interface lookup.
|
|
246
|
+
#
|
|
247
|
+
# @return [Integer, nil] The positive index, or nil if the entry is missing, unreadable, or invalid
|
|
156
248
|
def interface_index_from_sysfs
|
|
157
249
|
ifindex_path = File.join("/sys/class/net", interface_name, "ifindex")
|
|
158
250
|
return nil unless File.file?(ifindex_path)
|
|
@@ -163,6 +255,14 @@ module CanMessenger
|
|
|
163
255
|
nil
|
|
164
256
|
end
|
|
165
257
|
|
|
258
|
+
# Packs the native Linux sockaddr_can structure used to bind a raw socket.
|
|
259
|
+
#
|
|
260
|
+
# Socket addresses always use host byte order, independently of the byte
|
|
261
|
+
# order selected for CAN identifiers in frame building and parsing.
|
|
262
|
+
#
|
|
263
|
+
# @param [Integer] ifindex The resolved Linux interface index
|
|
264
|
+
# @return [String] The 24-byte packed socket address
|
|
265
|
+
# @raise [RuntimeError] If the packed layout does not have the expected size
|
|
166
266
|
def build_sockaddr_can(ifindex)
|
|
167
267
|
can_family = Socket.const_defined?(:AF_CAN) ? Socket::AF_CAN : Socket::PF_CAN
|
|
168
268
|
sockaddr = [can_family, ifindex, ZERO_CAN_ADDR].pack("S!x#{SOCKADDR_CAN_PADDING}i!a#{SOCKADDR_CAN_ADDR_SIZE}")
|
|
@@ -171,6 +271,12 @@ module CanMessenger
|
|
|
171
271
|
raise "sockaddr_can must be #{SOCKADDR_CAN_SIZE} bytes, got #{sockaddr.bytesize}"
|
|
172
272
|
end
|
|
173
273
|
|
|
274
|
+
# Checks that an identifier fits the selected CAN frame format without flags.
|
|
275
|
+
#
|
|
276
|
+
# @param [Object] id The caller-supplied identifier to validate
|
|
277
|
+
# @param [Boolean] extended_id Whether the allowed range is 29 bits instead of 11 bits
|
|
278
|
+
# @return [void]
|
|
279
|
+
# @raise [ArgumentError] If the identifier is not an integer or lies outside the allowed range
|
|
174
280
|
def validate_can_id!(id, extended_id:)
|
|
175
281
|
raise ArgumentError, "id must be an Integer" unless id.is_a?(Integer)
|
|
176
282
|
raise ArgumentError, "CAN id cannot be negative" if id.negative?
|
|
@@ -180,6 +286,21 @@ module CanMessenger
|
|
|
180
286
|
|
|
181
287
|
raise ArgumentError, "#{extended_id ? "Extended" : "Standard"} CAN id cannot exceed 0x#{max_id.to_s(16).upcase}"
|
|
182
288
|
end
|
|
289
|
+
|
|
290
|
+
# Checks the payload container and each byte before binary packing.
|
|
291
|
+
#
|
|
292
|
+
# Explicit validation prevents Array#pack from silently wrapping integers
|
|
293
|
+
# outside the byte range or coercing noninteger values.
|
|
294
|
+
#
|
|
295
|
+
# @param [Object] data The caller-supplied payload to validate
|
|
296
|
+
# @return [void]
|
|
297
|
+
# @raise [ArgumentError] If the payload is not an array of integers in 0..255
|
|
298
|
+
def validate_data!(data)
|
|
299
|
+
raise ArgumentError, "data must be an Array of byte Integers" unless data.is_a?(Array)
|
|
300
|
+
return if data.all? { |byte| byte.is_a?(Integer) && byte.between?(0, 255) }
|
|
301
|
+
|
|
302
|
+
raise ArgumentError, "data bytes must be Integers between 0 and 255"
|
|
303
|
+
end
|
|
183
304
|
end
|
|
184
305
|
end
|
|
185
306
|
end
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CanMessenger
|
|
4
|
+
# Shared CAN identifier masks and frame flags.
|
|
5
|
+
#
|
|
6
|
+
# SocketCAN stores frame flags above the 29 identifier bits. DBC message IDs
|
|
7
|
+
# also use bit 31 to mark extended frames, so the same flag is used when
|
|
8
|
+
# translating between DBC definitions and received frames.
|
|
4
9
|
module Constants
|
|
10
|
+
# Keeps only the 29 identifier bits and removes the frame flags.
|
|
5
11
|
CAN_ID_MASK = 0x1FFFFFFF
|
|
12
|
+
# Bit 31 selects the extended frame format with a 29-bit identifier.
|
|
6
13
|
EXTENDED_ID_FLAG = 0x80000000
|
|
14
|
+
# Bit 30 marks a remote request for data, which carries no signal payload.
|
|
15
|
+
REMOTE_TRANSMISSION_REQUEST_FLAG = 0x40000000
|
|
16
|
+
# Largest valid extended identifier before any frame flags are added.
|
|
7
17
|
MAX_EXTENDED_ID = 0x1FFFFFFF
|
|
8
18
|
end
|
|
9
19
|
end
|
data/lib/can_messenger/dbc.rb
CHANGED
|
@@ -6,6 +6,8 @@ module CanMessenger
|
|
|
6
6
|
# This class provides functionality to parse DBC files and encode/decode CAN messages
|
|
7
7
|
# according to the signal definitions. DBC files are a standard way to describe
|
|
8
8
|
# CAN network communication.
|
|
9
|
+
# Integer signals support signed or unsigned values, both byte orders, and scaling.
|
|
10
|
+
# Unsupported floating-point signal declarations raise instead of being decoded as integers.
|
|
9
11
|
#
|
|
10
12
|
# @example Loading and using a DBC file
|
|
11
13
|
# dbc = CanMessenger::DBC.load('vehicle.dbc')
|
|
@@ -17,7 +19,8 @@ module CanMessenger
|
|
|
17
19
|
# # Decode a received CAN frame
|
|
18
20
|
# decoded = dbc.decode_can(0x123, [0x09, 0xC4, 0xAB, 0x00, 0x00, 0x00, 0x00, 0x00])
|
|
19
21
|
# # => { name: 'EngineData', signals: { RPM: 2500.0, Temperature: 85.5 } }
|
|
20
|
-
class DBC
|
|
22
|
+
class DBC # rubocop:disable Metrics/ClassLength
|
|
23
|
+
# Metadata directive names that can be skipped; matching uses complete tokens.
|
|
21
24
|
IGNORED_LINE_PREFIXES = %w[
|
|
22
25
|
BO_TX_BU_
|
|
23
26
|
VERSION
|
|
@@ -29,12 +32,19 @@ module CanMessenger
|
|
|
29
32
|
BA_
|
|
30
33
|
BA_DEF_
|
|
31
34
|
BA_DEF_DEF_
|
|
32
|
-
SIG_VALTYPE_
|
|
33
35
|
].freeze
|
|
34
36
|
|
|
37
|
+
# Numeric tokens allow a sign, decimal point, and optional scientific exponent.
|
|
38
|
+
NUMBER_PATTERN = /[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/
|
|
39
|
+
# Message headers provide the stored identifier, name, payload size, and sender.
|
|
35
40
|
MESSAGE_LINE_PATTERN = /^BO_\s+(\d+)\s+(\w+)\s*:\s*(\d+)\s+\S.*$/
|
|
36
|
-
|
|
41
|
+
# Capture the signal layout and scaling; byte order is restricted to @0 or @1.
|
|
42
|
+
SIGNAL_LINE_PATTERN = /^SG_\s+(\w+)\s*:\s*(\d+)\|(\d+)@([01])([+-])\s*
|
|
43
|
+
\(\s*(#{NUMBER_PATTERN})\s*,\s*(#{NUMBER_PATTERN})\s*\)/x
|
|
44
|
+
# Type 0 is an integer; types 1 and 2 declare unsupported IEEE float and double signals.
|
|
45
|
+
SIGNAL_VALUE_TYPE_PATTERN = /\ASIG_VALTYPE_\s+\d+\s+\w+\s*:\s*(\d+)\s*;\z/
|
|
37
46
|
|
|
47
|
+
# @return [Hash<String, Message>] Parsed message definitions keyed by their names
|
|
38
48
|
attr_reader :messages
|
|
39
49
|
|
|
40
50
|
# Loads a DBC file from disk and parses its contents.
|
|
@@ -50,6 +60,8 @@ module CanMessenger
|
|
|
50
60
|
# Initializes a new DBC instance.
|
|
51
61
|
#
|
|
52
62
|
# @param [String] content The DBC file content to parse (optional)
|
|
63
|
+
# @return [void]
|
|
64
|
+
# @raise [ArgumentError] If the content contains a malformed or unsupported definition
|
|
53
65
|
def initialize(content = "")
|
|
54
66
|
@messages = {}
|
|
55
67
|
parse(content) unless content.empty?
|
|
@@ -60,9 +72,11 @@ module CanMessenger
|
|
|
60
72
|
# This method processes each line of the DBC content, identifying message
|
|
61
73
|
# definitions (BO_) and signal definitions (SG_). It builds a complete
|
|
62
74
|
# message structure with all associated signals.
|
|
75
|
+
# Existing messages are retained unless a new definition uses the same name.
|
|
63
76
|
#
|
|
64
77
|
# @param [String] content The DBC file content to parse
|
|
65
78
|
# @return [void]
|
|
79
|
+
# @raise [ArgumentError] If a definition is malformed or unsupported, with line number and text
|
|
66
80
|
def parse(content) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
67
81
|
current = nil
|
|
68
82
|
in_namespace_section = false
|
|
@@ -70,11 +84,10 @@ module CanMessenger
|
|
|
70
84
|
# The parser is intentionally strict so malformed DBC files fail early with line context.
|
|
71
85
|
# We still ignore a small allowlist of standard metadata directives that do not affect encode/decode.
|
|
72
86
|
content.each_line.with_index(1) do |line, line_number|
|
|
73
|
-
|
|
74
|
-
stripped_line = raw_line.strip
|
|
87
|
+
stripped_line = line.strip
|
|
75
88
|
|
|
76
89
|
if in_namespace_section
|
|
77
|
-
next if
|
|
90
|
+
next if line.match?(/^\s+\S/)
|
|
78
91
|
|
|
79
92
|
in_namespace_section = false
|
|
80
93
|
end
|
|
@@ -99,6 +112,8 @@ module CanMessenger
|
|
|
99
112
|
raise_parse_error("Invalid signal definition", line_number, stripped_line) unless sig
|
|
100
113
|
|
|
101
114
|
current.signals << sig
|
|
115
|
+
elsif stripped_line.start_with?("SIG_VALTYPE_")
|
|
116
|
+
validate_signal_value_type(stripped_line, line_number)
|
|
102
117
|
else
|
|
103
118
|
raise_parse_error("Unsupported or malformed DBC line", line_number, stripped_line)
|
|
104
119
|
end
|
|
@@ -127,24 +142,21 @@ module CanMessenger
|
|
|
127
142
|
#
|
|
128
143
|
# @param [String] line A single line from the DBC file
|
|
129
144
|
# @param [Message] _current The current message being processed (unused but kept for API consistency)
|
|
130
|
-
# @return [Signal, nil]
|
|
145
|
+
# @return [Signal, nil] The signal, or nil if its layout or scaling cannot be parsed
|
|
131
146
|
def parse_signal_line(line, _current) # rubocop:disable Metrics/MethodLength
|
|
132
147
|
return unless (m = line.match(SIGNAL_LINE_PATTERN))
|
|
133
148
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
sign = m[5] == "-" ? :signed : :unsigned
|
|
139
|
-
factor = m[6].to_f
|
|
140
|
-
offset = m[7].to_f
|
|
149
|
+
factor = parse_number(m[6])
|
|
150
|
+
offset = parse_number(m[7])
|
|
151
|
+
return unless factor && offset
|
|
152
|
+
return if factor.zero?
|
|
141
153
|
|
|
142
154
|
Signal.new(
|
|
143
|
-
|
|
144
|
-
start_bit:
|
|
145
|
-
length:
|
|
146
|
-
endianness:
|
|
147
|
-
sign:
|
|
155
|
+
m[1],
|
|
156
|
+
start_bit: m[2].to_i,
|
|
157
|
+
length: m[3].to_i,
|
|
158
|
+
endianness: m[4] == "1" ? :little : :big,
|
|
159
|
+
sign: m[5] == "-" ? :signed : :unsigned,
|
|
148
160
|
factor: factor,
|
|
149
161
|
offset: offset
|
|
150
162
|
)
|
|
@@ -158,7 +170,7 @@ module CanMessenger
|
|
|
158
170
|
# @param [String] name The name of the message to encode
|
|
159
171
|
# @param [Hash<Symbol|String, Numeric>] values Signal names mapped to their values
|
|
160
172
|
# @return [Hash] A hash containing :id (Integer) and :data (Array<Integer>)
|
|
161
|
-
# @raise [ArgumentError] If the
|
|
173
|
+
# @raise [ArgumentError] If the name is unknown, a value is out of range, or a signal exceeds the payload
|
|
162
174
|
#
|
|
163
175
|
# @example
|
|
164
176
|
# frame = dbc.encode_can('EngineData', RPM: 2500, Temperature: 85.5)
|
|
@@ -174,10 +186,13 @@ module CanMessenger
|
|
|
174
186
|
#
|
|
175
187
|
# Takes a CAN ID and data bytes, finds the matching message definition,
|
|
176
188
|
# and decodes the data into individual signal values according to the DBC.
|
|
189
|
+
# The identifier must match the stored DBC ID, including its extended-frame
|
|
190
|
+
# flag when present; Messenger handles this conversion for received frames.
|
|
177
191
|
#
|
|
178
192
|
# @param [Integer] id The CAN message ID
|
|
179
193
|
# @param [Array<Integer>] data The CAN message data bytes
|
|
180
194
|
# @return [Hash, nil] A hash containing :name (String) and :signals (Hash), or nil if no matching message
|
|
195
|
+
# @raise [ArgumentError] If a signal references bits outside the supplied payload
|
|
181
196
|
#
|
|
182
197
|
# @example
|
|
183
198
|
# decoded = dbc.decode_can(0x123, [0x09, 0xC4, 0xAB, 0x00, 0x00, 0x00, 0x00, 0x00])
|
|
@@ -191,10 +206,58 @@ module CanMessenger
|
|
|
191
206
|
|
|
192
207
|
private
|
|
193
208
|
|
|
209
|
+
# Checks whether a line begins with an explicitly supported metadata directive.
|
|
210
|
+
#
|
|
211
|
+
# Comparing the complete token prevents unsupported names such as BA_UNKNOWN_
|
|
212
|
+
# from being mistaken for the supported BA_ directive.
|
|
213
|
+
#
|
|
214
|
+
# @param [String] line The stripped DBC line
|
|
215
|
+
# @return [Boolean] Whether the parser can skip this directive
|
|
194
216
|
def ignored_line?(line)
|
|
195
|
-
|
|
217
|
+
IGNORED_LINE_PREFIXES.include?(line.split(/[\s:]/, 2).first)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Parses a scaling token without losing precision for integral values.
|
|
221
|
+
#
|
|
222
|
+
# Decimal and exponent spellings such as "1.0" and "1e0" remain exact integers.
|
|
223
|
+
# Fractional values use Float to retain the existing scaling behavior.
|
|
224
|
+
#
|
|
225
|
+
# @param [String] value A numeric token matched by the signal pattern
|
|
226
|
+
# @return [Integer, Float, nil] The parsed value, or nil if a fractional value is not finite
|
|
227
|
+
def parse_number(value)
|
|
228
|
+
value = value.sub(/\.(?=[eE]|\z)/, ".0")
|
|
229
|
+
exact = Rational(value)
|
|
230
|
+
return exact.to_i if exact.denominator == 1
|
|
231
|
+
|
|
232
|
+
number = Float(value)
|
|
233
|
+
number if number.finite?
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Validates a SIG_VALTYPE_ declaration and rejects unsupported signal representations.
|
|
237
|
+
#
|
|
238
|
+
# Only integer type 0 is supported. Accepting IEEE types 1 or 2 without handling
|
|
239
|
+
# their binary representation would silently change the encoded or decoded value.
|
|
240
|
+
#
|
|
241
|
+
# @param [String] line The stripped signal-type declaration
|
|
242
|
+
# @param [Integer] line_number The one-based line number in the DBC content
|
|
243
|
+
# @return [void]
|
|
244
|
+
# @raise [ArgumentError] If the declaration is malformed or names an unsupported type
|
|
245
|
+
def validate_signal_value_type(line, line_number)
|
|
246
|
+
match = line.match(SIGNAL_VALUE_TYPE_PATTERN)
|
|
247
|
+
raise_parse_error("Invalid signal value type definition", line_number, line) unless match
|
|
248
|
+
return if match[1] == "0"
|
|
249
|
+
|
|
250
|
+
raise_parse_error("Unsupported signal value type #{match[1]} (only integer type 0 is supported)",
|
|
251
|
+
line_number, line)
|
|
196
252
|
end
|
|
197
253
|
|
|
254
|
+
# Raises a parse error with enough source context to locate the definition.
|
|
255
|
+
#
|
|
256
|
+
# @param [String] reason The explanation of the parse failure
|
|
257
|
+
# @param [Integer] line_number The one-based line number in the DBC content
|
|
258
|
+
# @param [String] line The offending DBC line
|
|
259
|
+
# @return [void]
|
|
260
|
+
# @raise [ArgumentError] Always, with the reason, line number, and offending text
|
|
198
261
|
def raise_parse_error(reason, line_number, line)
|
|
199
262
|
raise ArgumentError, "#{reason} at line #{line_number}: #{line}"
|
|
200
263
|
end
|
|
@@ -214,9 +277,10 @@ module CanMessenger
|
|
|
214
277
|
|
|
215
278
|
# Initializes a new Message instance.
|
|
216
279
|
#
|
|
217
|
-
# @param [Integer] id The CAN
|
|
280
|
+
# @param [Integer] id The stored CAN ID, including the DBC extended-frame flag when present
|
|
218
281
|
# @param [String] name The symbolic name of the message
|
|
219
|
-
# @param [Integer] dlc
|
|
282
|
+
# @param [Integer] dlc Payload size in bytes; at most 8 for classic CAN or 64 for CAN FD
|
|
283
|
+
# @return [void]
|
|
220
284
|
def initialize(id, name, dlc)
|
|
221
285
|
@id = id
|
|
222
286
|
@name = name
|
|
@@ -228,9 +292,11 @@ module CanMessenger
|
|
|
228
292
|
#
|
|
229
293
|
# Iterates through all signals in the message and encodes their values
|
|
230
294
|
# into the appropriate bit positions within the message data bytes.
|
|
295
|
+
# The new payload is filled with zeros, and only supplied signal values are written.
|
|
231
296
|
#
|
|
232
297
|
# @param [Hash<Symbol|String, Numeric>] values Signal names mapped to their values
|
|
233
298
|
# @return [Array<Integer>] Array of bytes representing the encoded message
|
|
299
|
+
# @raise [ArgumentError] If a value is out of range or a signal exceeds the payload size
|
|
234
300
|
def encode(values)
|
|
235
301
|
bytes = Array.new(@dlc, 0)
|
|
236
302
|
@signals.each do |sig|
|
|
@@ -249,7 +315,8 @@ module CanMessenger
|
|
|
249
315
|
# the final engineering unit values.
|
|
250
316
|
#
|
|
251
317
|
# @param [Array<Integer>] data The message data bytes to decode
|
|
252
|
-
# @return [Hash<Symbol,
|
|
318
|
+
# @return [Hash<Symbol, Numeric>] Signal names mapped to their decoded values
|
|
319
|
+
# @raise [ArgumentError] If a signal references bits outside the supplied payload
|
|
253
320
|
def decode(data)
|
|
254
321
|
res = {}
|
|
255
322
|
@signals.each do |sig|
|
|
@@ -285,16 +352,17 @@ module CanMessenger
|
|
|
285
352
|
# @param [Integer] length The number of bits the signal occupies (1-64)
|
|
286
353
|
# @param [Symbol] endianness Byte order - :little for little-endian, :big for big-endian
|
|
287
354
|
# @param [Symbol] sign Value type - :unsigned for unsigned integers, :signed for signed integers
|
|
288
|
-
# @param [
|
|
289
|
-
# @param [
|
|
355
|
+
# @param [Numeric] factor Scaling factor to convert raw value to engineering units
|
|
356
|
+
# @param [Numeric] offset Offset to add after scaling
|
|
357
|
+
# @return [void]
|
|
290
358
|
def initialize(name, start_bit:, length:, endianness:, sign:, factor:, offset:) # rubocop:disable Metrics/ParameterLists
|
|
291
359
|
@name = name
|
|
292
360
|
@start_bit = start_bit
|
|
293
361
|
@length = length
|
|
294
362
|
@endianness = endianness
|
|
295
363
|
@sign = sign
|
|
296
|
-
@factor = factor
|
|
297
|
-
@offset = offset
|
|
364
|
+
@factor = normalize_integral_number(factor)
|
|
365
|
+
@offset = normalize_integral_number(offset)
|
|
298
366
|
end
|
|
299
367
|
|
|
300
368
|
# Encodes a value into the message byte array at this signal's bit position.
|
|
@@ -302,13 +370,14 @@ module CanMessenger
|
|
|
302
370
|
# Converts the engineering unit value to a raw integer using the signal's
|
|
303
371
|
# factor and offset, then places the bits into the appropriate positions
|
|
304
372
|
# within the message bytes.
|
|
373
|
+
# Fractional raw values round to the nearest integer, with ties away from zero.
|
|
305
374
|
#
|
|
306
375
|
# @param [Array<Integer>] bytes The message byte array to modify
|
|
307
376
|
# @param [Numeric] value The engineering unit value to encode
|
|
308
377
|
# @return [void]
|
|
309
378
|
# @raise [ArgumentError] If the value is out of range or signal exceeds message bounds
|
|
310
379
|
def encode(bytes, value)
|
|
311
|
-
raw = (
|
|
380
|
+
raw = (value - offset).quo(factor).round
|
|
312
381
|
validate_signal_bounds(bytes.size)
|
|
313
382
|
insert_bits(bytes, raw)
|
|
314
383
|
end
|
|
@@ -318,9 +387,11 @@ module CanMessenger
|
|
|
318
387
|
# Extracts the raw integer value from the appropriate bit positions,
|
|
319
388
|
# then applies the signal's scaling (factor and offset) to convert
|
|
320
389
|
# it to engineering units.
|
|
390
|
+
# Integer factors and offsets preserve exact Integer results; fractional Float scaling returns a Float.
|
|
321
391
|
#
|
|
322
392
|
# @param [Array<Integer>] bytes The message byte array to decode from
|
|
323
|
-
# @return [
|
|
393
|
+
# @return [Numeric] The decoded value in engineering units
|
|
394
|
+
# @raise [ArgumentError] If a signal bit falls outside the supplied payload
|
|
324
395
|
def decode(bytes)
|
|
325
396
|
raw = extract_bits(bytes)
|
|
326
397
|
(raw * factor) + offset
|
|
@@ -328,6 +399,19 @@ module CanMessenger
|
|
|
328
399
|
|
|
329
400
|
private
|
|
330
401
|
|
|
402
|
+
# Converts integral Float scaling values to Integer without changing fractional values.
|
|
403
|
+
#
|
|
404
|
+
# Keeping identity scaling such as 1.0 and 0.0 integral avoids rounding large
|
|
405
|
+
# signal values through floating-point arithmetic.
|
|
406
|
+
#
|
|
407
|
+
# @param [Numeric] number The factor or offset supplied to the signal constructor
|
|
408
|
+
# @return [Numeric] An Integer for integral finite Floats, otherwise the original number
|
|
409
|
+
def normalize_integral_number(number)
|
|
410
|
+
return number unless number.is_a?(Float) && number.finite? && number.denominator == 1
|
|
411
|
+
|
|
412
|
+
number.to_i
|
|
413
|
+
end
|
|
414
|
+
|
|
331
415
|
# Validates that the signal fits within the message boundaries.
|
|
332
416
|
#
|
|
333
417
|
# Ensures that all bits used by this signal fall within the message's
|
|
@@ -565,10 +649,25 @@ module CanMessenger
|
|
|
565
649
|
msb_set ? value - (1 << length) : value
|
|
566
650
|
end
|
|
567
651
|
|
|
652
|
+
# Reverses the bit numbering within each byte for Motorola signal positions.
|
|
653
|
+
#
|
|
654
|
+
# DBC sawtooth numbering labels a byte's least significant bit as 0, whereas
|
|
655
|
+
# sequential network numbering starts with its most significant bit. Applying
|
|
656
|
+
# this conversion twice returns the original position.
|
|
657
|
+
#
|
|
658
|
+
# @param [Integer] bitnum The absolute bit position in either numbering scheme
|
|
659
|
+
# @return [Integer] The corresponding position in the other scheme
|
|
568
660
|
def sawtooth_to_network_bitnum(bitnum)
|
|
569
661
|
(8 * (bitnum / 8)) + (7 - (bitnum % 8))
|
|
570
662
|
end
|
|
571
663
|
|
|
664
|
+
# Finds the first signal bit that falls outside the payload boundaries.
|
|
665
|
+
#
|
|
666
|
+
# Checks mapped positions so Motorola signals spanning multiple bytes use
|
|
667
|
+
# the same bounds rules as their encoding and decoding paths.
|
|
668
|
+
#
|
|
669
|
+
# @param [Integer] max_allowed_bit The highest valid bit index in the payload
|
|
670
|
+
# @return [Integer, nil] The first invalid position encountered, or nil if all positions fit
|
|
572
671
|
def invalid_bit_position(max_allowed_bit)
|
|
573
672
|
length.times do |i|
|
|
574
673
|
bit_pos = calculate_bit_position(i)
|
|
@@ -578,6 +677,10 @@ module CanMessenger
|
|
|
578
677
|
nil
|
|
579
678
|
end
|
|
580
679
|
|
|
680
|
+
# Converts a payload size to its highest valid zero-based bit index.
|
|
681
|
+
#
|
|
682
|
+
# @param [Integer] message_size_bytes The payload size in bytes
|
|
683
|
+
# @return [Integer] The highest bit index, or -1 for an empty payload
|
|
581
684
|
def max_allowed_bit_for(message_size_bytes)
|
|
582
685
|
(message_size_bytes * 8) - 1
|
|
583
686
|
end
|
|
@@ -132,6 +132,7 @@ module CanMessenger
|
|
|
132
132
|
# @return [void]
|
|
133
133
|
def process_message(socket, filter, can_fd, dbc, &block)
|
|
134
134
|
message = @adapter.receive_message(socket: socket, can_fd: can_fd)
|
|
135
|
+
return unless @listening
|
|
135
136
|
return if message.nil?
|
|
136
137
|
return if filter && !matches_filter?(message_id: message[:id], filter: filter)
|
|
137
138
|
|
|
@@ -186,6 +187,8 @@ module CanMessenger
|
|
|
186
187
|
end
|
|
187
188
|
|
|
188
189
|
def attach_dbc_payload!(message, dbc)
|
|
190
|
+
return if message[:remote]
|
|
191
|
+
|
|
189
192
|
decoded = dbc.decode_can(dbc_decode_id(message), message[:data])
|
|
190
193
|
message[:decoded] = decoded if decoded
|
|
191
194
|
rescue StandardError => e
|
data/lib/can_messenger.rb
CHANGED
|
@@ -6,6 +6,9 @@ require_relative "can_messenger/constants"
|
|
|
6
6
|
require_relative "can_messenger/messenger"
|
|
7
7
|
require_relative "can_messenger/dbc"
|
|
8
8
|
|
|
9
|
+
# Public namespace for CAN messaging, transport adapters, and DBC encoding/decoding.
|
|
10
|
+
# Requiring "can_messenger" loads these components together.
|
|
9
11
|
module CanMessenger
|
|
12
|
+
# Library-specific error type available to callers and adapter extensions.
|
|
10
13
|
class Error < StandardError; end
|
|
11
14
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module CanMessenger
|
|
2
|
+
module Adapter
|
|
3
|
+
class Base
|
|
4
|
+
@interface_name: String
|
|
5
|
+
@logger: Logger
|
|
6
|
+
@endianness: Symbol
|
|
7
|
+
|
|
8
|
+
attr_reader interface_name: String
|
|
9
|
+
attr_reader logger: Logger
|
|
10
|
+
attr_reader endianness: Symbol
|
|
11
|
+
|
|
12
|
+
def self.native_endianness: () -> Symbol
|
|
13
|
+
|
|
14
|
+
def initialize: (
|
|
15
|
+
interface_name: String,
|
|
16
|
+
logger: Logger,
|
|
17
|
+
?endianness: (String | Symbol)
|
|
18
|
+
) -> void
|
|
19
|
+
|
|
20
|
+
def open_socket: (?can_fd: bool?) -> untyped
|
|
21
|
+
def build_can_frame: (
|
|
22
|
+
id: Integer,
|
|
23
|
+
data: Array[Integer],
|
|
24
|
+
?extended_id: bool,
|
|
25
|
+
?can_fd: bool?
|
|
26
|
+
) -> String
|
|
27
|
+
def receive_message: (
|
|
28
|
+
socket: untyped,
|
|
29
|
+
?can_fd: bool?
|
|
30
|
+
) -> ({ id: Integer, data: Array[Integer], extended: bool, ?remote: bool, ?dlc: Integer } | nil)
|
|
31
|
+
def parse_frame: (
|
|
32
|
+
frame: String,
|
|
33
|
+
?can_fd: bool?
|
|
34
|
+
) -> ({ id: Integer, data: Array[Integer], extended: bool, ?remote: bool, ?dlc: Integer } | nil)
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def self.pack_uint: (Integer value, String template) -> String
|
|
39
|
+
def normalize_endianness: ((String | Symbol) endianness) -> Symbol
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module CanMessenger
|
|
2
|
+
module Adapter
|
|
3
|
+
class Socketcan < Base
|
|
4
|
+
CAN_RAW: Integer
|
|
5
|
+
SOL_CAN_BASE: Integer
|
|
6
|
+
SOL_CAN_RAW: Integer
|
|
7
|
+
CAN_RAW_FD_FRAMES: Integer
|
|
8
|
+
FRAME_SIZE: Integer
|
|
9
|
+
CANFD_FRAME_SIZE: Integer
|
|
10
|
+
MIN_FRAME_SIZE: Integer
|
|
11
|
+
MAX_FD_DATA: Integer
|
|
12
|
+
MAX_STANDARD_ID: Integer
|
|
13
|
+
SOCKADDR_CAN_ADDR_SIZE: Integer
|
|
14
|
+
SOCKADDR_CAN_SIZE: Integer
|
|
15
|
+
SOCKADDR_CAN_PADDING: Integer
|
|
16
|
+
TIMEOUT: String
|
|
17
|
+
ZERO_CAN_ADDR: String
|
|
18
|
+
|
|
19
|
+
def open_socket: (?can_fd: bool?) -> untyped
|
|
20
|
+
def build_can_frame: (
|
|
21
|
+
id: Integer,
|
|
22
|
+
data: Array[Integer],
|
|
23
|
+
?extended_id: bool,
|
|
24
|
+
?can_fd: bool?
|
|
25
|
+
) -> String
|
|
26
|
+
def receive_message: (
|
|
27
|
+
socket: untyped,
|
|
28
|
+
?can_fd: bool?
|
|
29
|
+
) -> ({ id: Integer, data: Array[Integer], extended: bool, ?remote: bool, ?dlc: Integer } | nil)
|
|
30
|
+
def parse_frame: (
|
|
31
|
+
frame: String,
|
|
32
|
+
?can_fd: bool?
|
|
33
|
+
) -> ({ id: Integer, data: Array[Integer], extended: bool, ?remote: bool, ?dlc: Integer } | nil)
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def frame_data_length: (String frame, can_fd: bool?) -> Integer
|
|
38
|
+
def unpack_frame_id: (frame: String) -> Integer
|
|
39
|
+
def configure_socket: (untyped socket, can_fd: bool?) -> void
|
|
40
|
+
def close_socket: (untyped socket) -> void
|
|
41
|
+
def interface_index: () -> Integer
|
|
42
|
+
def interface_index_from_ifaddrs: () -> Integer?
|
|
43
|
+
def interface_index_from_sysfs: () -> Integer?
|
|
44
|
+
def build_sockaddr_can: (Integer ifindex) -> String
|
|
45
|
+
def validate_can_id!: (untyped id, extended_id: bool) -> void
|
|
46
|
+
def validate_data!: (untyped data) -> void
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
module CanMessenger
|
|
2
|
+
# DBC (Database CAN) Parser and Encoder/Decoder
|
|
3
|
+
class DBC
|
|
4
|
+
IGNORED_LINE_PREFIXES: Array[String]
|
|
5
|
+
NUMBER_PATTERN: Regexp
|
|
6
|
+
MESSAGE_LINE_PATTERN: Regexp
|
|
7
|
+
SIGNAL_LINE_PATTERN: Regexp
|
|
8
|
+
SIGNAL_VALUE_TYPE_PATTERN: Regexp
|
|
9
|
+
|
|
10
|
+
@messages: Hash[String, Message]
|
|
11
|
+
|
|
12
|
+
attr_reader messages: Hash[String, Message]
|
|
13
|
+
|
|
14
|
+
# Loads a DBC file from disk and parses its contents
|
|
15
|
+
def self.load: (String path) -> DBC
|
|
16
|
+
|
|
17
|
+
# Initializes a new DBC instance
|
|
18
|
+
def initialize: (?String content) -> void
|
|
19
|
+
|
|
20
|
+
# Parses DBC content and populates the messages hash
|
|
21
|
+
def parse: (String content) -> void
|
|
22
|
+
|
|
23
|
+
# Parses a message definition line from DBC content
|
|
24
|
+
def parse_message_line: (String line) -> (Message | nil)
|
|
25
|
+
|
|
26
|
+
# Parses a signal definition line from DBC content
|
|
27
|
+
def parse_signal_line: (String line, Message? _current) -> (Signal | nil)
|
|
28
|
+
|
|
29
|
+
# Encodes signal values into a CAN message frame
|
|
30
|
+
def encode_can: (String name, Hash[Symbol | String, Numeric] values) -> { id: Integer, data: Array[Integer] }
|
|
31
|
+
|
|
32
|
+
# Decodes a CAN message frame into signal values
|
|
33
|
+
def decode_can: (Integer id, Array[Integer] data) -> ({ name: String, signals: Hash[Symbol, Numeric] } | nil)
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def ignored_line?: (String line) -> bool
|
|
38
|
+
def parse_number: (String value) -> (Integer | Float | nil)
|
|
39
|
+
def validate_signal_value_type: (String line, Integer line_number) -> void
|
|
40
|
+
def raise_parse_error: (String reason, Integer line_number, String line) -> void
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Represents a CAN message definition from a DBC file
|
|
44
|
+
class Message
|
|
45
|
+
@id: Integer
|
|
46
|
+
@name: String
|
|
47
|
+
@dlc: Integer
|
|
48
|
+
@signals: Array[Signal]
|
|
49
|
+
|
|
50
|
+
attr_reader id: Integer
|
|
51
|
+
attr_reader name: String
|
|
52
|
+
attr_reader dlc: Integer
|
|
53
|
+
attr_reader signals: Array[Signal]
|
|
54
|
+
|
|
55
|
+
# Initializes a new Message instance
|
|
56
|
+
def initialize: (Integer id, String name, Integer dlc) -> void
|
|
57
|
+
|
|
58
|
+
# Encodes signal values into the message byte array
|
|
59
|
+
def encode: (Hash[Symbol | String, Numeric] values) -> Array[Integer]
|
|
60
|
+
|
|
61
|
+
# Decodes message data bytes into individual signal values
|
|
62
|
+
def decode: (Array[Integer] data) -> Hash[Symbol, Numeric]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Represents a signal within a CAN message
|
|
66
|
+
class Signal
|
|
67
|
+
@name: String
|
|
68
|
+
@start_bit: Integer
|
|
69
|
+
@length: Integer
|
|
70
|
+
@endianness: Symbol
|
|
71
|
+
@sign: Symbol
|
|
72
|
+
@factor: Numeric
|
|
73
|
+
@offset: Numeric
|
|
74
|
+
|
|
75
|
+
attr_reader name: String
|
|
76
|
+
attr_reader start_bit: Integer
|
|
77
|
+
attr_reader length: Integer
|
|
78
|
+
attr_reader endianness: Symbol
|
|
79
|
+
attr_reader sign: Symbol
|
|
80
|
+
attr_reader factor: Numeric
|
|
81
|
+
attr_reader offset: Numeric
|
|
82
|
+
|
|
83
|
+
# Initializes a new Signal instance
|
|
84
|
+
def initialize: (
|
|
85
|
+
String name,
|
|
86
|
+
start_bit: Integer,
|
|
87
|
+
length: Integer,
|
|
88
|
+
endianness: Symbol,
|
|
89
|
+
sign: Symbol,
|
|
90
|
+
factor: Numeric,
|
|
91
|
+
offset: Numeric
|
|
92
|
+
) -> void
|
|
93
|
+
|
|
94
|
+
# Encodes a value into the message byte array at this signal's bit position
|
|
95
|
+
def encode: (Array[Integer] bytes, Numeric value) -> void
|
|
96
|
+
|
|
97
|
+
# Decodes this signal's value from the message byte array
|
|
98
|
+
def decode: (Array[Integer] bytes) -> Numeric
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def normalize_integral_number: (Numeric number) -> Numeric
|
|
103
|
+
|
|
104
|
+
# Validates that the signal fits within the message boundaries
|
|
105
|
+
def validate_signal_bounds: (Integer message_size_bytes) -> void
|
|
106
|
+
|
|
107
|
+
# Encodes a raw integer value into the message byte array
|
|
108
|
+
def insert_bits: (Array[Integer] bytes, Integer raw) -> void
|
|
109
|
+
|
|
110
|
+
# Validates the raw integer value before encoding
|
|
111
|
+
def validate_raw_value: (Integer raw) -> void
|
|
112
|
+
|
|
113
|
+
# Validates unsigned values to ensure they're not negative
|
|
114
|
+
def validate_unsigned_value: (Integer raw) -> void
|
|
115
|
+
|
|
116
|
+
# Validates signed values to ensure they fit in the signal's bit range
|
|
117
|
+
def validate_signed_value: (Integer raw) -> void
|
|
118
|
+
|
|
119
|
+
# Processes the raw value for encoding (handles two's complement conversion)
|
|
120
|
+
def process_raw_value: (Integer raw) -> Integer
|
|
121
|
+
|
|
122
|
+
# Writes the processed bits into the message byte array
|
|
123
|
+
def write_bits_to_bytes: (Array[Integer] bytes, Integer raw) -> void
|
|
124
|
+
|
|
125
|
+
# Calculates the bit position for a given bit offset within the signal
|
|
126
|
+
def calculate_bit_position: (Integer bit_offset) -> Integer
|
|
127
|
+
|
|
128
|
+
# Calculates byte and bit indices from an absolute bit position
|
|
129
|
+
def calculate_byte_and_bit_indices: (Integer bit_pos) -> [Integer, Integer]
|
|
130
|
+
|
|
131
|
+
# Validates that a bit position is within the message boundaries
|
|
132
|
+
def validate_bit_position: (Integer bit_pos, Integer bytes_size) -> void
|
|
133
|
+
|
|
134
|
+
# Updates a specific bit within a byte in the message array
|
|
135
|
+
def update_byte_with_bit: (Array[Integer] bytes, Integer byte_index, Integer bit_index, Integer bit) -> void
|
|
136
|
+
|
|
137
|
+
# Extracts the signal value from the message byte array
|
|
138
|
+
def extract_bits: (Array[Integer] bytes) -> Integer
|
|
139
|
+
|
|
140
|
+
# Reads the raw bits from the message byte array
|
|
141
|
+
def read_bits_from_bytes: (Array[Integer] bytes) -> Integer
|
|
142
|
+
|
|
143
|
+
# Validates bit position during extraction to ensure it's within bounds
|
|
144
|
+
def validate_extraction_bounds: (Integer bit_pos, Integer bytes_size) -> void
|
|
145
|
+
|
|
146
|
+
# Converts unsigned value to signed if the signal is signed and the MSB is set
|
|
147
|
+
def convert_to_signed_if_needed: (Integer value) -> Integer
|
|
148
|
+
|
|
149
|
+
# Converts a sawtooth bit number to sequential network bit numbering
|
|
150
|
+
def sawtooth_to_network_bitnum: (Integer bitnum) -> Integer
|
|
151
|
+
|
|
152
|
+
# Returns the first invalid bit position for this signal, if any
|
|
153
|
+
def invalid_bit_position: (Integer max_allowed_bit) -> (Integer | nil)
|
|
154
|
+
|
|
155
|
+
# Calculates the maximum valid bit index for a message size
|
|
156
|
+
def max_allowed_bit_for: (Integer message_size_bytes) -> Integer
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module CanMessenger
|
|
2
|
+
# Messenger class for CAN bus communication
|
|
3
|
+
class Messenger
|
|
4
|
+
@interface_name: String
|
|
5
|
+
@logger: Logger
|
|
6
|
+
@listening: bool
|
|
7
|
+
@endianness: (String | Symbol)
|
|
8
|
+
@can_fd: bool
|
|
9
|
+
@adapter: untyped
|
|
10
|
+
|
|
11
|
+
# Initializes a new Messenger instance
|
|
12
|
+
def initialize: (
|
|
13
|
+
interface_name: String,
|
|
14
|
+
?logger: Logger?,
|
|
15
|
+
?endianness: (String | Symbol),
|
|
16
|
+
?can_fd: bool,
|
|
17
|
+
?adapter: untyped
|
|
18
|
+
) -> void
|
|
19
|
+
|
|
20
|
+
# Sends a CAN message by writing directly to a raw CAN socket
|
|
21
|
+
def send_can_message: (
|
|
22
|
+
id: Integer,
|
|
23
|
+
data: Array[Integer],
|
|
24
|
+
?extended_id: bool,
|
|
25
|
+
?can_fd: bool?
|
|
26
|
+
) -> void
|
|
27
|
+
|
|
28
|
+
# Encodes and sends a CAN message using a DBC definition
|
|
29
|
+
def send_dbc_message: (
|
|
30
|
+
message_name: String,
|
|
31
|
+
signals: Hash[Symbol | String, Numeric],
|
|
32
|
+
?dbc: CanMessenger::DBC?,
|
|
33
|
+
?extended_id: bool,
|
|
34
|
+
?can_fd: bool?
|
|
35
|
+
) -> void
|
|
36
|
+
|
|
37
|
+
# Continuously listens for CAN messages on the specified interface
|
|
38
|
+
def start_listening: (
|
|
39
|
+
?filter: (Integer | Range[Integer] | Array[Integer])?,
|
|
40
|
+
?can_fd: bool?,
|
|
41
|
+
?dbc: CanMessenger::DBC?
|
|
42
|
+
) { ({ id: Integer, data: Array[Integer], extended: bool, ?remote: bool, ?dlc: Integer, ?decoded: { name: String, signals: Hash[Symbol, Numeric] }, ?decode_error: { class: String, message: String } }) -> untyped } -> void
|
|
43
|
+
|
|
44
|
+
# Stops the listening loop
|
|
45
|
+
def stop_listening: () -> void
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Opens a socket, yields it, and closes it when done
|
|
50
|
+
def with_socket: (?can_fd: bool?) { (untyped socket) -> untyped } -> void
|
|
51
|
+
|
|
52
|
+
# Processes a single CAN message from socket
|
|
53
|
+
def process_message: (
|
|
54
|
+
untyped socket,
|
|
55
|
+
(Integer | Range[Integer] | Array[Integer])? filter,
|
|
56
|
+
bool can_fd,
|
|
57
|
+
CanMessenger::DBC? dbc
|
|
58
|
+
) { ({ id: Integer, data: Array[Integer], extended: bool, ?remote: bool, ?dlc: Integer, ?decoded: { name: String, signals: Hash[Symbol, Numeric] }, ?decode_error: { class: String, message: String } }) -> untyped } -> void
|
|
59
|
+
|
|
60
|
+
# Checks whether the given message ID matches the specified filter
|
|
61
|
+
def matches_filter?: (message_id: Integer, filter: (Integer | Range[Integer] | Array[Integer])?) -> bool
|
|
62
|
+
|
|
63
|
+
def validate_filter!: ((Integer | Range[Integer] | Array[Integer])?) -> void
|
|
64
|
+
def validate_range_filter!: (Range[Integer] filter) -> void
|
|
65
|
+
def validate_array_filter!: (Array[Integer] filter) -> void
|
|
66
|
+
def attach_dbc_payload!: (
|
|
67
|
+
{ id: Integer, data: Array[Integer], extended: bool, ?remote: bool, ?dlc: Integer, ?decoded: { name: String, signals: Hash[Symbol, Numeric] }, ?decode_error: { class: String, message: String } } message,
|
|
68
|
+
CanMessenger::DBC dbc
|
|
69
|
+
) -> void
|
|
70
|
+
def normalize_can_id: (Integer id) -> Integer
|
|
71
|
+
def dbc_extended_id?: (Integer id) -> bool
|
|
72
|
+
def normalized_dbc_message: (
|
|
73
|
+
{ id: Integer, data: Array[Integer] } encoded,
|
|
74
|
+
extended_id: bool
|
|
75
|
+
) -> { id: Integer, data: Array[Integer], extended_id: bool }
|
|
76
|
+
def dbc_decode_id: ({ id: Integer, extended: bool, ?remote: bool, ?dlc: Integer } message) -> Integer
|
|
77
|
+
end
|
|
78
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: can_messenger
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.3.
|
|
4
|
+
version: 2.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- fk1018
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: logger
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.6'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.6'
|
|
12
26
|
description: CanMessenger provides an interface to send and receive messages over
|
|
13
27
|
the CAN bus, useful for applications requiring CAN communication in Ruby.
|
|
14
28
|
email:
|
|
@@ -27,6 +41,13 @@ files:
|
|
|
27
41
|
- lib/can_messenger/dbc.rb
|
|
28
42
|
- lib/can_messenger/messenger.rb
|
|
29
43
|
- lib/can_messenger/version.rb
|
|
44
|
+
- sig/can_messenger.rbs
|
|
45
|
+
- sig/can_messenger/adapter/base.rbs
|
|
46
|
+
- sig/can_messenger/adapter/socketcan.rbs
|
|
47
|
+
- sig/can_messenger/constants.rbs
|
|
48
|
+
- sig/can_messenger/dbc.rbs
|
|
49
|
+
- sig/can_messenger/messenger.rbs
|
|
50
|
+
- sig/can_messenger/version.rbs
|
|
30
51
|
homepage: https://can-messenger.github.io/
|
|
31
52
|
licenses:
|
|
32
53
|
- MIT
|
|
@@ -43,14 +64,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
43
64
|
requirements:
|
|
44
65
|
- - ">="
|
|
45
66
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: 4.0.
|
|
67
|
+
version: 4.0.9
|
|
47
68
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
69
|
requirements:
|
|
49
70
|
- - ">="
|
|
50
71
|
- !ruby/object:Gem::Version
|
|
51
72
|
version: '0'
|
|
52
73
|
requirements: []
|
|
53
|
-
rubygems_version: 4.0.
|
|
74
|
+
rubygems_version: 4.0.20
|
|
54
75
|
specification_version: 4
|
|
55
76
|
summary: A simple Ruby wrapper to read and write CAN bus messages.
|
|
56
77
|
test_files: []
|