mavlink 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +149 -0
- data/Rakefile +12 -0
- data/lib/mavlink/crc.rb +21 -0
- data/lib/mavlink/frame.rb +73 -0
- data/lib/mavlink/message.rb +134 -0
- data/lib/mavlink/parser.rb +77 -0
- data/lib/mavlink/version.rb +5 -0
- data/lib/mavlink.rb +14 -0
- metadata +84 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 53f52062bc7c9418b318763b74bdd79e29b0cac403b29fd1401b8930eb7f8d3a
|
|
4
|
+
data.tar.gz: 92928da3f63698734bfb5ea85840e2f876c89535294a559a1f3ee36cbfb894e6
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 20c3b3be194b748a413ced15a052debe077f0bd6a5b9ea368e83205c811852167c3c0ac6a841865391a818f9ca4025060719b88dc649ddda9fa6dea0b80a22fa
|
|
7
|
+
data.tar.gz: d7360441513fe8435886b0581d88e52f4a17cc4f79b15c5c8865efcabff5bd6143419cd7579f8f832174477fb375c3248a382becff720cde0810ee1705c22614
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thomas Smith
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# MAVLink Ruby
|
|
2
|
+
|
|
3
|
+
A compact MAVLink implementation for Ruby. It packs and unpacks MAVLink 1 and MAVLink 2 frames, checks CRC-16/MCRF4XX checksums, and scans a byte stream for the start of a frame.
|
|
4
|
+
|
|
5
|
+
Requires Ruby 3.2 or newer.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "mavlink"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
And then execute:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or install it yourself:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
gem install mavlink
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require "mavlink"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Payloads are raw bytes. This gem frames those bytes. It does not decode dialect fields such as `HEARTBEAT.type`. Pass the message's `CRC_EXTRA` byte into `pack` and `unpack`. `MAVLink::CRC_EXTRA_HEARTBEAT` is `50`.
|
|
34
|
+
|
|
35
|
+
### Pack and unpack a frame
|
|
36
|
+
|
|
37
|
+
A frame is a header, a payload, and a checksum. `pack` recomputes the checksum and appends it. `unpack` raises `ArgumentError` when the length or checksum is wrong.
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
payload = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03].pack("C*")
|
|
41
|
+
|
|
42
|
+
header = MAVLink::Message.new(
|
|
43
|
+
payload_length: payload.bytesize,
|
|
44
|
+
sequence: 0,
|
|
45
|
+
system_id: 1,
|
|
46
|
+
component_id: 1,
|
|
47
|
+
message_id: 0
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
wire = MAVLink::Frame.new(header: header, payload: payload, checksum: 0)
|
|
51
|
+
.pack(crc_extra: MAVLink::CRC_EXTRA_HEARTBEAT)
|
|
52
|
+
|
|
53
|
+
frame = MAVLink::Frame.unpack(wire, crc_extra: MAVLink::CRC_EXTRA_HEARTBEAT)
|
|
54
|
+
frame.header.message_id # => 0
|
|
55
|
+
frame.payload # => the 9 payload bytes
|
|
56
|
+
frame.checksum # => 0xA1B1
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`Frame.new` checks that `payload.bytesize` matches `header.payload_length` and that `checksum` is an integer in `0..0xFFFF`.
|
|
60
|
+
|
|
61
|
+
### Headers
|
|
62
|
+
|
|
63
|
+
`MAVLink::Message` is the packet header. MAVLink 2 is the default (`stx` `0xFD`, 10 bytes, 24-bit `message_id`). MAVLink 1 uses `stx: MAVLink::STX_V1` (`0xFE`, 6 bytes, `message_id` in `0..255`).
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
header = MAVLink::Message.new(
|
|
67
|
+
payload_length: 9,
|
|
68
|
+
sequence: 0,
|
|
69
|
+
system_id: 1,
|
|
70
|
+
component_id: 1,
|
|
71
|
+
message_id: 0
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
header.mavlink2? # => true
|
|
75
|
+
header.pack # => 10-byte binary header
|
|
76
|
+
MAVLink::Message.unpack(header.pack) # => header
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
MAVLink 1:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
header = MAVLink::Message.new(
|
|
83
|
+
stx: MAVLink::STX_V1,
|
|
84
|
+
payload_length: 9,
|
|
85
|
+
sequence: 0,
|
|
86
|
+
system_id: 1,
|
|
87
|
+
component_id: 1,
|
|
88
|
+
message_id: 0
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
header.pack.bytesize # => 6
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Header fields:
|
|
95
|
+
|
|
96
|
+
| Field | Range |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `stx` | `STX_V1` (`0xFE`) or `STX_V2` (`0xFD`) |
|
|
99
|
+
| `payload_length` | `0..255` |
|
|
100
|
+
| `incompatibility_flags` | `0..255` (must be `0` for MAVLink 1) |
|
|
101
|
+
| `compatibility_flags` | `0..255` (must be `0` for MAVLink 1) |
|
|
102
|
+
| `sequence` | `0..255` |
|
|
103
|
+
| `system_id` | `0..255` |
|
|
104
|
+
| `component_id` | `0..255` |
|
|
105
|
+
| `message_id` | `0..0xFFFFFF` (`0..255` for MAVLink 1) |
|
|
106
|
+
|
|
107
|
+
`Message.unpack` expects exactly 10 bytes for MAVLink 2 and exactly 6 bytes for MAVLink 1.
|
|
108
|
+
|
|
109
|
+
### Scan a byte stream
|
|
110
|
+
|
|
111
|
+
`MAVLink::Parser` reads a continuous stream. It drops bytes until it sees `0xFE` or `0xFD`, then keeps that byte and everything after it. `feed` accepts a binary `String` or one integer in `0..255`, and returns the parser. `<<` is an alias.
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
parser = MAVLink::Parser.new
|
|
115
|
+
parser.waiting_for_stx? # => true
|
|
116
|
+
|
|
117
|
+
parser.feed([0x00, 0x11, MAVLink::STX_V2, 0x09].pack("C*"))
|
|
118
|
+
parser.got_stx? # => true
|
|
119
|
+
parser.stx # => 0xFD
|
|
120
|
+
parser.buffer # => bytes from 0xFD onward
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`buffer` is a copy. The parser does not yet split that buffer into frames. Once a full frame is buffered, decode it with `MAVLink::Frame.unpack`.
|
|
124
|
+
|
|
125
|
+
### Checksum
|
|
126
|
+
|
|
127
|
+
`MAVLink::CRC` is CRC-16/MCRF4XX: init `0xFFFF`, no final XOR. The check vector `"123456789"` is `0x6F91`.
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
MAVLink::CRC.calculate("123456789") # => 0x6F91
|
|
131
|
+
|
|
132
|
+
crc = MAVLink::CRC::INIT
|
|
133
|
+
crc = MAVLink::CRC.accumulate(byte, crc)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
A frame checksum covers the header without the magic byte, the payload, and one extra `CRC_EXTRA` byte that depends on the message id.
|
|
137
|
+
|
|
138
|
+
## Development
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
bin/setup
|
|
142
|
+
bundle exec rake test
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`bundle exec rake build` writes the package to `pkg/`. `bundle exec rake release` tags the current version and pushes it to RubyGems.
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT. See [LICENSE](LICENSE).
|
data/Rakefile
ADDED
data/lib/mavlink/crc.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MAVLink
|
|
4
|
+
# CRC-16/MCRF4XX (MAVLink checksum)
|
|
5
|
+
# Init 0xFFFF, no final XOR.
|
|
6
|
+
module CRC
|
|
7
|
+
INIT = 0xFFFF
|
|
8
|
+
|
|
9
|
+
def self.accumulate(byte, crc)
|
|
10
|
+
tmp = (byte ^ (crc & 0xFF)) & 0xFF
|
|
11
|
+
tmp = (tmp ^ (tmp << 4)) & 0xFF
|
|
12
|
+
((crc >> 8) ^ (tmp << 8) ^ (tmp << 3) ^ (tmp >> 4)) & 0xFFFF
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.calculate(data)
|
|
16
|
+
crc = INIT
|
|
17
|
+
data.each_byte { |byte| crc = accumulate(byte, crc) }
|
|
18
|
+
crc
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MAVLink
|
|
4
|
+
# Full MAVLink packet: header + payload + CRC-16 checksum
|
|
5
|
+
Frame = ::Data.define(:header, :payload, :checksum) do
|
|
6
|
+
HEADER_LEN_V1 = 6
|
|
7
|
+
HEADER_LEN_V2 = 10
|
|
8
|
+
CHECKSUM_LEN = 2
|
|
9
|
+
|
|
10
|
+
def initialize(header:, payload:, checksum:)
|
|
11
|
+
raise ArgumentError, "header must be a MAVLink::Message, got #{header.class}" unless header.is_a?(Message)
|
|
12
|
+
raise ArgumentError, "payload must be a String, got #{payload.class}" unless payload.is_a?(String)
|
|
13
|
+
unless payload.bytesize == header.payload_length
|
|
14
|
+
raise ArgumentError,
|
|
15
|
+
"payload length #{payload.bytesize} does not match header.payload_length #{header.payload_length}"
|
|
16
|
+
end
|
|
17
|
+
unless checksum.is_a?(Integer) && checksum.between?(0, 0xFFFF)
|
|
18
|
+
raise ArgumentError, "checksum must be an integer in 0..0xFFFF, got #{checksum.inspect}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
super(header: header, payload: payload.b, checksum: checksum)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def pack(crc_extra:)
|
|
25
|
+
header_bytes = header.pack
|
|
26
|
+
ck = self.class.compute_checksum(header_bytes, payload, crc_extra)
|
|
27
|
+
header_bytes + payload + [ck & 0xFF, (ck >> 8) & 0xFF].pack("C*")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.unpack(binary, crc_extra:)
|
|
31
|
+
raise ArgumentError, "binary must be a String, got #{binary.class}" unless binary.is_a?(String)
|
|
32
|
+
raise ArgumentError, "binary is empty" if binary.empty?
|
|
33
|
+
|
|
34
|
+
stx = binary.getbyte(0)
|
|
35
|
+
header_len = header_length_for(stx)
|
|
36
|
+
raise ArgumentError, "frame too short for header, got #{binary.bytesize} bytes" if binary.bytesize < header_len
|
|
37
|
+
|
|
38
|
+
payload_length = binary.getbyte(1)
|
|
39
|
+
expected = header_len + payload_length + CHECKSUM_LEN
|
|
40
|
+
unless binary.bytesize == expected
|
|
41
|
+
raise ArgumentError, "frame requires #{expected} bytes, got #{binary.bytesize}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
header = Message.unpack(binary.byteslice(0, header_len))
|
|
45
|
+
payload = binary.byteslice(header_len, payload_length).b
|
|
46
|
+
checksum = binary.getbyte(header_len + payload_length) |
|
|
47
|
+
(binary.getbyte(header_len + payload_length + 1) << 8)
|
|
48
|
+
|
|
49
|
+
expected_checksum = compute_checksum(binary.byteslice(0, header_len), payload, crc_extra)
|
|
50
|
+
unless checksum == expected_checksum
|
|
51
|
+
raise ArgumentError,
|
|
52
|
+
"checksum mismatch: got 0x#{checksum.to_s(16)}, expected 0x#{expected_checksum.to_s(16)}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
new(header: header, payload: payload, checksum: checksum)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.compute_checksum(header_bytes, payload, crc_extra)
|
|
59
|
+
crc = CRC.calculate(header_bytes.byteslice(1, header_bytes.bytesize - 1) + payload)
|
|
60
|
+
CRC.accumulate(crc_extra, crc)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.header_length_for(stx)
|
|
64
|
+
case stx
|
|
65
|
+
when STX_V2 then HEADER_LEN_V2
|
|
66
|
+
when STX_V1 then HEADER_LEN_V1
|
|
67
|
+
else
|
|
68
|
+
raise ArgumentError, "stx must be STX_V1 (0xFE) or STX_V2 (0xFD), got #{stx.inspect}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
private_class_method :header_length_for
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MAVLink
|
|
4
|
+
# Immutable MAVLink packet header (v1 or v2 framing)
|
|
5
|
+
Message = ::Data.define(
|
|
6
|
+
:stx,
|
|
7
|
+
:payload_length,
|
|
8
|
+
:incompatibility_flags,
|
|
9
|
+
:compatibility_flags,
|
|
10
|
+
:sequence,
|
|
11
|
+
:system_id,
|
|
12
|
+
:component_id,
|
|
13
|
+
:message_id
|
|
14
|
+
) do
|
|
15
|
+
def initialize(
|
|
16
|
+
stx: STX_V2,
|
|
17
|
+
payload_length:,
|
|
18
|
+
incompatibility_flags: 0,
|
|
19
|
+
compatibility_flags: 0,
|
|
20
|
+
sequence:,
|
|
21
|
+
system_id:,
|
|
22
|
+
component_id:,
|
|
23
|
+
message_id:
|
|
24
|
+
)
|
|
25
|
+
validate_stx!(stx)
|
|
26
|
+
validate_uint8!(:payload_length, payload_length)
|
|
27
|
+
validate_uint8!(:incompatibility_flags, incompatibility_flags)
|
|
28
|
+
validate_uint8!(:compatibility_flags, compatibility_flags)
|
|
29
|
+
validate_uint8!(:sequence, sequence)
|
|
30
|
+
validate_uint8!(:system_id, system_id)
|
|
31
|
+
validate_uint8!(:component_id, component_id)
|
|
32
|
+
validate_message_id!(message_id)
|
|
33
|
+
validate_v1_constraints!(stx, incompatibility_flags, compatibility_flags, message_id)
|
|
34
|
+
|
|
35
|
+
super(
|
|
36
|
+
stx: stx,
|
|
37
|
+
payload_length: payload_length,
|
|
38
|
+
incompatibility_flags: incompatibility_flags,
|
|
39
|
+
compatibility_flags: compatibility_flags,
|
|
40
|
+
sequence: sequence,
|
|
41
|
+
system_id: system_id,
|
|
42
|
+
component_id: component_id,
|
|
43
|
+
message_id: message_id
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def mavlink2?
|
|
48
|
+
stx == STX_V2
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def mavlink1?
|
|
52
|
+
stx == STX_V1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def pack
|
|
56
|
+
if mavlink2?
|
|
57
|
+
[
|
|
58
|
+
stx, payload_length, incompatibility_flags, compatibility_flags,
|
|
59
|
+
sequence, system_id, component_id, message_id & 0xFF,
|
|
60
|
+
(message_id >> 8) & 0xFF, (message_id >> 16) & 0xFF
|
|
61
|
+
].pack("C*")
|
|
62
|
+
else
|
|
63
|
+
[stx, payload_length, sequence, system_id, component_id, message_id].pack("C*")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.unpack(binary)
|
|
68
|
+
raise ArgumentError, "binary must be a String, got #{binary.class}" unless binary.is_a?(String)
|
|
69
|
+
raise ArgumentError, "binary is empty" if binary.empty?
|
|
70
|
+
|
|
71
|
+
stx = binary.getbyte(0)
|
|
72
|
+
case stx
|
|
73
|
+
when STX_V2
|
|
74
|
+
raise ArgumentError, "MAVLink 2 header requires 10 bytes, got #{binary.bytesize}" unless binary.bytesize == 10
|
|
75
|
+
b = binary.unpack("C10")
|
|
76
|
+
new(
|
|
77
|
+
stx: b[0],
|
|
78
|
+
payload_length: b[1],
|
|
79
|
+
incompatibility_flags: b[2],
|
|
80
|
+
compatibility_flags: b[3],
|
|
81
|
+
sequence: b[4],
|
|
82
|
+
system_id: b[5],
|
|
83
|
+
component_id: b[6],
|
|
84
|
+
message_id: b[7] | (b[8] << 8) | (b[9] << 16)
|
|
85
|
+
)
|
|
86
|
+
when STX_V1
|
|
87
|
+
raise ArgumentError, "MAVLink 1 header requires 6 bytes, got #{binary.bytesize}" unless binary.bytesize == 6
|
|
88
|
+
b = binary.unpack("C6")
|
|
89
|
+
new(
|
|
90
|
+
stx: b[0],
|
|
91
|
+
payload_length: b[1],
|
|
92
|
+
sequence: b[2],
|
|
93
|
+
system_id: b[3],
|
|
94
|
+
component_id: b[4],
|
|
95
|
+
message_id: b[5]
|
|
96
|
+
)
|
|
97
|
+
else
|
|
98
|
+
raise ArgumentError, "stx must be STX_V1 (0xFE) or STX_V2 (0xFD), got #{stx.inspect}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def validate_stx!(stx)
|
|
105
|
+
return if stx == STX_V1 || stx == STX_V2
|
|
106
|
+
|
|
107
|
+
raise ArgumentError, "stx must be STX_V1 (0xFE) or STX_V2 (0xFD), got #{stx.inspect}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def validate_uint8!(name, value)
|
|
111
|
+
return if value.is_a?(Integer) && value.between?(0, 255)
|
|
112
|
+
|
|
113
|
+
raise ArgumentError, "#{name} must be an integer in 0..255, got #{value.inspect}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def validate_message_id!(message_id)
|
|
117
|
+
return if message_id.is_a?(Integer) && message_id.between?(0, 0xFFFFFF)
|
|
118
|
+
|
|
119
|
+
raise ArgumentError, "message_id must be an integer in 0..0xFFFFFF, got #{message_id.inspect}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def validate_v1_constraints!(stx, incompatibility_flags, compatibility_flags, message_id)
|
|
123
|
+
return unless stx == STX_V1
|
|
124
|
+
|
|
125
|
+
if incompatibility_flags != 0 || compatibility_flags != 0
|
|
126
|
+
raise ArgumentError, "MAVLink 1 headers must have zero incompatibility_flags and compatibility_flags"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
return if message_id <= 255
|
|
130
|
+
|
|
131
|
+
raise ArgumentError, "MAVLink 1 message_id must be in 0..255, got #{message_id.inspect}"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MAVLink
|
|
4
|
+
# Incremental state machine for a continuous MAVLink byte stream.
|
|
5
|
+
#
|
|
6
|
+
# Starts in WAITING_FOR_STX and discards bytes until a start-of-frame
|
|
7
|
+
# marker arrives: STX_V1 (0xFE) or STX_V2 (0xFD). That byte is kept and
|
|
8
|
+
# the machine moves to GOT_STX. Later bytes are retained and not treated
|
|
9
|
+
# as a new search.
|
|
10
|
+
class Parser
|
|
11
|
+
WAITING_FOR_STX = :waiting_for_stx
|
|
12
|
+
GOT_STX = :got_stx
|
|
13
|
+
|
|
14
|
+
attr_reader :state, :stx
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@state = WAITING_FOR_STX
|
|
18
|
+
@stx = nil
|
|
19
|
+
@buffer = String.new(encoding: Encoding::BINARY)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def waiting_for_stx?
|
|
23
|
+
state == WAITING_FOR_STX
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def got_stx?
|
|
27
|
+
state == GOT_STX
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Bytes kept from the start-of-frame onward. Empty while still scanning.
|
|
31
|
+
def buffer
|
|
32
|
+
@buffer.dup
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Consume a String of bytes, or a single Integer byte in 0..255.
|
|
36
|
+
# Returns self so chunks can be chained.
|
|
37
|
+
def feed(data)
|
|
38
|
+
each_input_byte(data) { |byte| accept(byte) }
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
alias << feed
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def accept(byte)
|
|
46
|
+
case state
|
|
47
|
+
when WAITING_FOR_STX
|
|
48
|
+
return unless start_of_frame?(byte)
|
|
49
|
+
|
|
50
|
+
@stx = byte
|
|
51
|
+
@buffer << byte
|
|
52
|
+
@state = GOT_STX
|
|
53
|
+
when GOT_STX
|
|
54
|
+
@buffer << byte
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def start_of_frame?(byte)
|
|
59
|
+
byte == STX_V1 || byte == STX_V2
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def each_input_byte(data)
|
|
63
|
+
case data
|
|
64
|
+
when String
|
|
65
|
+
data.each_byte { |byte| yield byte }
|
|
66
|
+
when Integer
|
|
67
|
+
unless data.between?(0, 255)
|
|
68
|
+
raise ArgumentError, "byte must be an integer in 0..255, got #{data}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
yield data
|
|
72
|
+
else
|
|
73
|
+
raise ArgumentError, "data must be a String or Integer byte, got #{data.class}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
data/lib/mavlink.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "mavlink/version"
|
|
4
|
+
|
|
5
|
+
module MAVLink
|
|
6
|
+
STX_V1 = 0xFE
|
|
7
|
+
STX_V2 = 0xFD
|
|
8
|
+
CRC_EXTRA_HEARTBEAT = 50
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
require_relative "mavlink/crc"
|
|
12
|
+
require_relative "mavlink/message"
|
|
13
|
+
require_relative "mavlink/frame"
|
|
14
|
+
require_relative "mavlink/parser"
|
metadata
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mavlink
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Thomas Smith
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-09-29 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: minitest
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '5.25'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '5.25'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '13.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '13.0'
|
|
41
|
+
description: A compact, high-performance implementation of the MAVLink protocol written
|
|
42
|
+
in Ruby.
|
|
43
|
+
email: []
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- LICENSE
|
|
49
|
+
- README.md
|
|
50
|
+
- Rakefile
|
|
51
|
+
- lib/mavlink.rb
|
|
52
|
+
- lib/mavlink/crc.rb
|
|
53
|
+
- lib/mavlink/frame.rb
|
|
54
|
+
- lib/mavlink/message.rb
|
|
55
|
+
- lib/mavlink/parser.rb
|
|
56
|
+
- lib/mavlink/version.rb
|
|
57
|
+
homepage: https://github.com/thomassmithdev/mavlink-ruby
|
|
58
|
+
licenses:
|
|
59
|
+
- MIT
|
|
60
|
+
metadata:
|
|
61
|
+
allowed_push_host: https://rubygems.org
|
|
62
|
+
homepage_uri: https://github.com/thomassmithdev/mavlink-ruby
|
|
63
|
+
source_code_uri: https://github.com/thomassmithdev/mavlink-ruby
|
|
64
|
+
rubygems_mfa_required: 'true'
|
|
65
|
+
post_install_message:
|
|
66
|
+
rdoc_options: []
|
|
67
|
+
require_paths:
|
|
68
|
+
- lib
|
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: 3.2.0
|
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: '0'
|
|
79
|
+
requirements: []
|
|
80
|
+
rubygems_version: 3.5.11
|
|
81
|
+
signing_key:
|
|
82
|
+
specification_version: 4
|
|
83
|
+
summary: A compact, high-performance MAVLink protocol implementation for Ruby
|
|
84
|
+
test_files: []
|