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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +13 -0
  3. data/.gitignore +25 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +49 -0
  8. data/examples/check_and_set_destination_address.rb +161 -0
  9. data/examples/read_frames.rb +45 -0
  10. data/examples/response_parser_throughput.rb +35 -0
  11. data/lib/xbee.rb +5 -0
  12. data/lib/xbee/address.rb +23 -0
  13. data/lib/xbee/address_16.rb +39 -0
  14. data/lib/xbee/address_64.rb +40 -0
  15. data/lib/xbee/bytes.rb +21 -0
  16. data/lib/xbee/exceptions/exception.rb +9 -0
  17. data/lib/xbee/exceptions/frame_format_error.rb +9 -0
  18. data/lib/xbee/exceptions/unknown_frame_type.rb +9 -0
  19. data/lib/xbee/frames/addressed_frame.rb +27 -0
  20. data/lib/xbee/frames/at_command.rb +35 -0
  21. data/lib/xbee/frames/at_command_queue_parameter_value.rb +21 -0
  22. data/lib/xbee/frames/at_command_response.rb +31 -0
  23. data/lib/xbee/frames/create_source_route.rb +15 -0
  24. data/lib/xbee/frames/data/sample.rb +72 -0
  25. data/lib/xbee/frames/data_sample_rx_indicator.rb +51 -0
  26. data/lib/xbee/frames/explicit_addressing_command.rb +68 -0
  27. data/lib/xbee/frames/explicit_rx_indicator.rb +60 -0
  28. data/lib/xbee/frames/frame.rb +65 -0
  29. data/lib/xbee/frames/identified_frame.rb +24 -0
  30. data/lib/xbee/frames/many_to_one_route_request_indicator.rb +11 -0
  31. data/lib/xbee/frames/modem_status.rb +47 -0
  32. data/lib/xbee/frames/node_identification_indicator.rb +38 -0
  33. data/lib/xbee/frames/over_the_air_firmware_update_status.rb +28 -0
  34. data/lib/xbee/frames/receive_packet.rb +44 -0
  35. data/lib/xbee/frames/remote_at_command_request.rb +53 -0
  36. data/lib/xbee/frames/remote_at_command_response.rb +45 -0
  37. data/lib/xbee/frames/route_record_indicator.rb +28 -0
  38. data/lib/xbee/frames/transmit_request.rb +70 -0
  39. data/lib/xbee/frames/transmit_status.rb +63 -0
  40. data/lib/xbee/frames/unidentified_addressed_frame.rb +26 -0
  41. data/lib/xbee/frames/xbee_sensor_read_indicator.rb +50 -0
  42. data/lib/xbee/packet.rb +175 -0
  43. data/lib/xbee/version.rb +5 -0
  44. data/lib/xbee/xbee.rb +109 -0
  45. data/xbee.gemspec +40 -0
  46. 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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XBee
4
+ VERSION = '1.0.2'
5
+ end
@@ -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
@@ -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