xbee 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|