xbee 1.0.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 +7 -0
- data/.editorconfig +13 -0
- data/.gitignore +25 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +49 -0
- data/examples/check_and_set_destination_address.rb +161 -0
- data/examples/read_frames.rb +45 -0
- data/examples/response_parser_throughput.rb +35 -0
- data/lib/xbee.rb +5 -0
- data/lib/xbee/address.rb +23 -0
- data/lib/xbee/address_16.rb +39 -0
- data/lib/xbee/address_64.rb +40 -0
- data/lib/xbee/bytes.rb +21 -0
- data/lib/xbee/exceptions/exception.rb +9 -0
- data/lib/xbee/exceptions/frame_format_error.rb +9 -0
- data/lib/xbee/exceptions/unknown_frame_type.rb +9 -0
- data/lib/xbee/frames/addressed_frame.rb +27 -0
- data/lib/xbee/frames/at_command.rb +35 -0
- data/lib/xbee/frames/at_command_queue_parameter_value.rb +21 -0
- data/lib/xbee/frames/at_command_response.rb +31 -0
- data/lib/xbee/frames/create_source_route.rb +15 -0
- data/lib/xbee/frames/data/sample.rb +72 -0
- data/lib/xbee/frames/data_sample_rx_indicator.rb +51 -0
- data/lib/xbee/frames/explicit_addressing_command.rb +68 -0
- data/lib/xbee/frames/explicit_rx_indicator.rb +60 -0
- data/lib/xbee/frames/frame.rb +65 -0
- data/lib/xbee/frames/identified_frame.rb +24 -0
- data/lib/xbee/frames/many_to_one_route_request_indicator.rb +11 -0
- data/lib/xbee/frames/modem_status.rb +47 -0
- data/lib/xbee/frames/node_identification_indicator.rb +38 -0
- data/lib/xbee/frames/over_the_air_firmware_update_status.rb +28 -0
- data/lib/xbee/frames/receive_packet.rb +44 -0
- data/lib/xbee/frames/remote_at_command_request.rb +53 -0
- data/lib/xbee/frames/remote_at_command_response.rb +45 -0
- data/lib/xbee/frames/route_record_indicator.rb +28 -0
- data/lib/xbee/frames/transmit_request.rb +70 -0
- data/lib/xbee/frames/transmit_status.rb +63 -0
- data/lib/xbee/frames/unidentified_addressed_frame.rb +26 -0
- data/lib/xbee/frames/xbee_sensor_read_indicator.rb +50 -0
- data/lib/xbee/packet.rb +175 -0
- data/lib/xbee/version.rb +5 -0
- data/lib/xbee/xbee.rb +109 -0
- data/xbee.gemspec +40 -0
- metadata +245 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'identified_frame'
|
3
|
+
|
4
|
+
module XBee
|
5
|
+
module Frames
|
6
|
+
# When a Transmit Request (0x10, 0x11) completes, the device sends a Transmit Status message out of
|
7
|
+
# the serial interface. This message indicates if the Transmit Request was successful or if it failed.
|
8
|
+
class TransmitStatus < IdentifiedFrame
|
9
|
+
api_id 0x8b
|
10
|
+
|
11
|
+
attr_accessor :address16
|
12
|
+
attr_accessor :retry_count
|
13
|
+
attr_accessor :delivery_status
|
14
|
+
attr_accessor :discovery_status
|
15
|
+
|
16
|
+
|
17
|
+
DELIVERY_STATUSES = {
|
18
|
+
0x00 => 'Success',
|
19
|
+
0x01 => 'MAC ACK Failure',
|
20
|
+
0x02 => 'CCA Failure',
|
21
|
+
0x15 => 'Invalid destination endpoint',
|
22
|
+
0x21 => 'Network ACK Failure',
|
23
|
+
0x22 => 'Not Joined to Network',
|
24
|
+
0x23 => 'Self-addressed',
|
25
|
+
0x24 => 'Address Not Found',
|
26
|
+
0x25 => 'Route Not Found',
|
27
|
+
0x26 => 'Broadcast source failed to hear a neighbor relay the message',
|
28
|
+
0x2B => 'Invalid binding table index',
|
29
|
+
0x2C => 'Resource error lack of free buffers, timers, etc.',
|
30
|
+
0x2D => 'Attempted broadcast with APS transmission',
|
31
|
+
0x2E => 'Attempted unicast with APS transmission, but EE=0',
|
32
|
+
0x32 => 'Resource error lack of free buffers, timers, etc.',
|
33
|
+
0x74 => 'Data payload too large',
|
34
|
+
0x75 => 'Indirect message unrequested',
|
35
|
+
}.freeze
|
36
|
+
|
37
|
+
DISCOVERY_STATUSES = {
|
38
|
+
0x00 => 'No Discovery Overhead',
|
39
|
+
0x01 => 'Address Discovery',
|
40
|
+
0x02 => 'Route Discovery',
|
41
|
+
0x03 => 'Address and Route',
|
42
|
+
0x40 => 'Extended Timeout Discovery',
|
43
|
+
}.freeze
|
44
|
+
|
45
|
+
|
46
|
+
def initialize(packet: nil)
|
47
|
+
super
|
48
|
+
|
49
|
+
if @parse_bytes
|
50
|
+
@address16 = Address16.new *@parse_bytes.shift(2)
|
51
|
+
@retry_count = @parse_bytes.shift
|
52
|
+
@delivery_status = @parse_bytes.shift
|
53
|
+
@discovery_status = @parse_bytes.shift
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
def bytes
|
59
|
+
super + (address16 || [0xff, 0xfd]).to_a + [retry_count || 0x00] + [delivery_status || 0x00] + [discovery_status || 0x00]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'frame'
|
3
|
+
|
4
|
+
module XBee
|
5
|
+
module Frames
|
6
|
+
class UnidentifiedAddressedFrame < Frame
|
7
|
+
attr_accessor :address16
|
8
|
+
attr_accessor :address64
|
9
|
+
|
10
|
+
|
11
|
+
def initialize(packet: nil)
|
12
|
+
super
|
13
|
+
|
14
|
+
if @parse_bytes
|
15
|
+
@address64 = Address64.new *@parse_bytes.shift(8)
|
16
|
+
@address16 = Address16.new *@parse_bytes.shift(2)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
def bytes
|
22
|
+
super + (address64 || Address64.from_array([0] * 8)).to_a + (address16 || Address16.new(0, 0)).to_a
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'frame'
|
3
|
+
require_relative '../bytes'
|
4
|
+
|
5
|
+
module XBee
|
6
|
+
module Frames
|
7
|
+
# When the device receives a sensor sample (from a Digi 1-wire sensor adapter), it is sent out the serial
|
8
|
+
# port using this message type (when AO=0).
|
9
|
+
class XBeeSensorReadIndicator < Frame
|
10
|
+
api_id 0x94
|
11
|
+
|
12
|
+
attr_accessor :address64
|
13
|
+
attr_accessor :address16
|
14
|
+
attr_accessor :options
|
15
|
+
attr_accessor :one_wire_sensors
|
16
|
+
attr_accessor :analog_values # Array of 4 integers
|
17
|
+
attr_accessor :temperature
|
18
|
+
|
19
|
+
|
20
|
+
def initialize(packet: nil)
|
21
|
+
super
|
22
|
+
|
23
|
+
if @parse_bytes
|
24
|
+
@address64 = Address64.new *@parse_bytes.shift(8)
|
25
|
+
@address16 = Address16.new *@parse_bytes.shift(2)
|
26
|
+
@options = @parse_bytes.shift
|
27
|
+
@one_wire_sensors = @parse_bytes.shift
|
28
|
+
@analog_values = [
|
29
|
+
Bytes.unsigned_int_from_array(@parse_bytes.shift(2)),
|
30
|
+
Bytes.unsigned_int_from_array(@parse_bytes.shift(2)),
|
31
|
+
Bytes.unsigned_int_from_array(@parse_bytes.shift(2)),
|
32
|
+
Bytes.unsigned_int_from_array(@parse_bytes.shift(2)),
|
33
|
+
]
|
34
|
+
@temperature = Bytes.unsigned_int_from_array(@parse_bytes.shift(2))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def bytes
|
40
|
+
super +
|
41
|
+
(address64 || Address64::COORDINATOR).to_a +
|
42
|
+
(address16 || Address16::COORDINATOR).to_a +
|
43
|
+
[options || 0x00] +
|
44
|
+
[one_wire_sensors || 0x00] +
|
45
|
+
((analog_values || [0xffff] * 4).map { |v| Bytes.array_from_unsigned_int(v, 2) }.reduce(:+))+
|
46
|
+
Bytes.array_from_unsigned_int(temperature || 0x0, 2)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/xbee/packet.rb
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module XBee
|
4
|
+
class Packet
|
5
|
+
START_BYTE = 0x7E
|
6
|
+
ESCAPE = 0x7D
|
7
|
+
XON = 0x11
|
8
|
+
XOFF = 0x13
|
9
|
+
|
10
|
+
ESCAPE_XOR = 0x20
|
11
|
+
|
12
|
+
ESCAPE_BYTES = [
|
13
|
+
START_BYTE, ESCAPE, XON, XOFF
|
14
|
+
].freeze
|
15
|
+
|
16
|
+
class << self
|
17
|
+
# @param byte [Integer]
|
18
|
+
def special_byte?(byte)
|
19
|
+
ESCAPE_BYTES.include? byte
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
def checksum(bytes)
|
24
|
+
255 - bytes.reduce(&:+) % 256
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
# Escapes an array of bytes. Ignores the first byte unless ignore_first_byte is set to false in the options hash.
|
29
|
+
# @param bytes [Array<Integer>] The array of bytes to escape.
|
30
|
+
# @param options [Hash] Options hash.
|
31
|
+
# @option options [Boolean] :ignore_first_byte If the first byte should be ignored (usually true for handling an entire packet since the first byte is START_BYTE). Default true.
|
32
|
+
# @return [Array<Integer>] Escaped bytes.
|
33
|
+
def escape(bytes, options = {})
|
34
|
+
ignore_first_byte = options.fetch :ignore_first_byte, true
|
35
|
+
|
36
|
+
prepend = []
|
37
|
+
if ignore_first_byte
|
38
|
+
bytes = bytes.dup
|
39
|
+
prepend = [bytes.shift]
|
40
|
+
end
|
41
|
+
|
42
|
+
prepend + bytes.reduce([]) do |escaped, b|
|
43
|
+
if ESCAPE_BYTES.include?(b)
|
44
|
+
escaped << ESCAPE
|
45
|
+
escaped << (ESCAPE_XOR ^ b)
|
46
|
+
else
|
47
|
+
escaped << b
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
# When provided a byte array that has escaped data, this returns a new byte array with just the raw data.
|
54
|
+
# @param bytes [Array<Integer>] Array of bytes to unescape.
|
55
|
+
# @return [Array<Integer>] Array of unescaped bytes.
|
56
|
+
def unescape(bytes)
|
57
|
+
byte_escaped = false
|
58
|
+
bytes.reduce([]) do |unescaped, b|
|
59
|
+
if byte_escaped
|
60
|
+
unescaped << (0x20 ^ b)
|
61
|
+
byte_escaped = false
|
62
|
+
else
|
63
|
+
if b == ESCAPE
|
64
|
+
byte_escaped = true
|
65
|
+
else
|
66
|
+
unescaped << b
|
67
|
+
end
|
68
|
+
end
|
69
|
+
unescaped
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
def from_bytes(bytes)
|
75
|
+
if bytes.length < 4
|
76
|
+
raise ArgumentError, "Packet is too short (only #{bytes.length} bytes)"
|
77
|
+
end
|
78
|
+
if bytes[0] != START_BYTE
|
79
|
+
raise ArgumentError, 'Missing start byte'
|
80
|
+
end
|
81
|
+
data = [START_BYTE] + unescape(bytes[1..-1])
|
82
|
+
length = (data[1] << 8) + data[2]
|
83
|
+
if length != data.length - 4
|
84
|
+
raise ArgumentError, "Expected data length to be #{length} but was #{data.length - 4}"
|
85
|
+
end
|
86
|
+
crc = checksum(data[3..-2])
|
87
|
+
if crc != data[-1]
|
88
|
+
raise ArgumentError, "Expected checksum to be 0x#{crc.to_s 16} but was 0x#{data[-1].to_s 16}"
|
89
|
+
end
|
90
|
+
new data[3..-2]
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
def next_unescaped_byte(bytes)
|
95
|
+
byte = bytes.next
|
96
|
+
if byte == ESCAPE
|
97
|
+
0x20 ^ bytes.next
|
98
|
+
else
|
99
|
+
byte
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
def from_byte_enum(bytes)
|
105
|
+
begin
|
106
|
+
loop until bytes.next == START_BYTE
|
107
|
+
length = (next_unescaped_byte(bytes) << 8) + next_unescaped_byte(bytes)
|
108
|
+
rescue
|
109
|
+
raise IOError, 'Packet is too short, unable to read length fields.'
|
110
|
+
end
|
111
|
+
begin
|
112
|
+
data = (1..length).map { next_unescaped_byte bytes }
|
113
|
+
rescue
|
114
|
+
raise IOError, "Expected data length to be #{length} but got fewer bytes"
|
115
|
+
end
|
116
|
+
begin
|
117
|
+
crc = next_unescaped_byte bytes
|
118
|
+
rescue
|
119
|
+
raise IOError, 'Packet is too short, unable to read checksum'
|
120
|
+
end
|
121
|
+
if crc != checksum(data)
|
122
|
+
raise IOError, "Expected checksum to be 0x#{checksum(data).to_s 16} but was 0x#{crc.to_s 16}"
|
123
|
+
end
|
124
|
+
new data
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
# @param data [Array<Integer>] Byte array
|
130
|
+
def initialize(data)
|
131
|
+
@data = data
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
def data
|
136
|
+
@data
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
def length
|
141
|
+
@data.length
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
def checksum
|
146
|
+
Packet.checksum @data
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
def bytes
|
151
|
+
[START_BYTE, length >> 8, length & 0xff] + @data + [checksum]
|
152
|
+
end
|
153
|
+
|
154
|
+
|
155
|
+
def bytes_escaped
|
156
|
+
[START_BYTE] + bytes[1..-1].flat_map do |b|
|
157
|
+
if self.class.special_byte?(b)
|
158
|
+
[ESCAPE, 0x20 ^ b]
|
159
|
+
else
|
160
|
+
b
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
def ==(other)
|
167
|
+
data == other.data
|
168
|
+
end
|
169
|
+
|
170
|
+
|
171
|
+
def to_s
|
172
|
+
'Packet [' + data.map { |b| "0x#{b.to_s 16}" }.join(', ') + ']'
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
data/lib/xbee/version.rb
ADDED
data/lib/xbee/xbee.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'semantic_logger'
|
3
|
+
|
4
|
+
require_relative 'packet'
|
5
|
+
require_relative 'frames/at_command'
|
6
|
+
require_relative 'frames/at_command_queue_parameter_value'
|
7
|
+
require_relative 'frames/at_command_response'
|
8
|
+
require_relative 'frames/create_source_route'
|
9
|
+
require_relative 'frames/data_sample_rx_indicator'
|
10
|
+
require_relative 'frames/explicit_addressing_command'
|
11
|
+
require_relative 'frames/explicit_rx_indicator'
|
12
|
+
require_relative 'frames/many_to_one_route_request_indicator'
|
13
|
+
require_relative 'frames/modem_status'
|
14
|
+
require_relative 'frames/node_identification_indicator'
|
15
|
+
require_relative 'frames/over_the_air_firmware_update_status'
|
16
|
+
require_relative 'frames/receive_packet'
|
17
|
+
require_relative 'frames/remote_at_command_request'
|
18
|
+
require_relative 'frames/remote_at_command_response'
|
19
|
+
require_relative 'frames/route_record_indicator'
|
20
|
+
require_relative 'frames/transmit_request'
|
21
|
+
require_relative 'frames/transmit_status'
|
22
|
+
require_relative 'frames/xbee_sensor_read_indicator'
|
23
|
+
|
24
|
+
|
25
|
+
module XBee
|
26
|
+
# Either specify the port and serial parameters
|
27
|
+
#
|
28
|
+
# xbee = XBee::Xbee.new device_path: '/dev/ttyUSB0', rate: 9600
|
29
|
+
#
|
30
|
+
# or pass in a SerialPort like object
|
31
|
+
#
|
32
|
+
# xbee = XBee::XBee.new io: some_serial_mockup_for_testing
|
33
|
+
#
|
34
|
+
class XBee
|
35
|
+
include SemanticLogger::Loggable
|
36
|
+
|
37
|
+
def initialize(device_path: '/dev/ttyUSB0', rate: 115200, io: nil)
|
38
|
+
@device_path = device_path
|
39
|
+
@rate = rate
|
40
|
+
@io = io
|
41
|
+
@connected = false
|
42
|
+
@logger = nil
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
def open
|
47
|
+
@io ||= SerialPort.new @device_path, @rate
|
48
|
+
@io_input = Enumerator.new do |y|
|
49
|
+
loop do
|
50
|
+
y.yield @io.readbyte
|
51
|
+
end
|
52
|
+
end
|
53
|
+
@connected = true
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
def close
|
58
|
+
@io.close if @io
|
59
|
+
@connected = false
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
def connected?
|
64
|
+
@connected
|
65
|
+
end
|
66
|
+
alias :open? :connected?
|
67
|
+
|
68
|
+
|
69
|
+
def write_packet(packet)
|
70
|
+
@io.write packet.bytes_escaped.pack('C*').force_encoding('ascii')
|
71
|
+
@io.flush
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
def write_frame(frame)
|
76
|
+
if frame.packet
|
77
|
+
# TODO: Is it right to assume the packet is in sync with the frame?
|
78
|
+
write_packet frame.packet
|
79
|
+
else
|
80
|
+
packet = frame.to_packet
|
81
|
+
write_packet packet
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
def write_request(request)
|
87
|
+
logger.measure_trace('Packet sent.', payload: { bytes: request.packet.bytes }) do
|
88
|
+
write_packet request.packet
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
def read_packet
|
94
|
+
Packet.from_byte_enum(@io_input).tap do |packet|
|
95
|
+
logger.trace 'Packet received.', bytes: packet.bytes
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
def read_frame
|
101
|
+
Frames::Frame.from_packet read_packet
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
def io=(io)
|
106
|
+
@io = io
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
data/xbee.gemspec
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
lib = File.expand_path('../lib', __FILE__)
|
5
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
6
|
+
require 'xbee/version'
|
7
|
+
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = 'xbee'
|
10
|
+
spec.version = XBee::VERSION
|
11
|
+
spec.description = 'A Ruby API for sending/receiving API frames with Digi XBee RF Modules.'
|
12
|
+
spec.summary = 'A Ruby API for Digi XBee RF Modules.'
|
13
|
+
spec.email = 'rubygems-xbee@aarontc.com'
|
14
|
+
spec.authors = ['Dirk Grappendorf (http://www.grappendorf.net)', 'Aaron Ten Clay (https://aarontc.com)']
|
15
|
+
spec.homepage = 'https://github.com/IdleEngineers/xbee'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
spec.metadata = {
|
18
|
+
'issue_tracker' => 'https://work.techtonium.com/jira/browse/XBEE',
|
19
|
+
}
|
20
|
+
|
21
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
22
|
+
f.match(%r{^(test|spec|features)/})
|
23
|
+
end
|
24
|
+
spec.bindir = 'exe'
|
25
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
26
|
+
spec.require_paths = ['lib']
|
27
|
+
|
28
|
+
spec.add_development_dependency 'bundler', '~> 1'
|
29
|
+
spec.add_development_dependency 'minitest', '~> 5'
|
30
|
+
spec.add_development_dependency 'minitest-ci', '~> 3'
|
31
|
+
spec.add_development_dependency 'minitest-reporters', '~> 1'
|
32
|
+
spec.add_development_dependency 'rake', '~> 10'
|
33
|
+
spec.add_development_dependency 'rr', '~> 1'
|
34
|
+
spec.add_development_dependency 'simplecov', '~> 0'
|
35
|
+
spec.add_development_dependency 'simplecov-rcov', '~> 0'
|
36
|
+
spec.add_development_dependency 'trollop', '~> 2'
|
37
|
+
|
38
|
+
spec.add_dependency 'serialport', '~> 1'
|
39
|
+
spec.add_dependency 'semantic_logger', '~> 4'
|
40
|
+
end
|