mqtt 0.0.3 → 0.0.4
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.
- data/NEWS +5 -0
- data/README +8 -7
- data/Rakefile +1 -1
- data/examples/livetext.rb +2 -2
- data/examples/readonly_proxy.rb +25 -0
- data/examples/simple_get.rb +1 -1
- data/examples/simple_publish.rb +1 -1
- data/lib/mqtt.rb +0 -21
- data/lib/mqtt/client.rb +31 -77
- data/lib/mqtt/packet.rb +497 -87
- data/lib/mqtt/proxy.rb +121 -0
- metadata +4 -2
data/NEWS
CHANGED
@@ -1,5 +1,10 @@
|
|
1
1
|
= Ruby MQTT NEWS
|
2
2
|
|
3
|
+
== Ruby MQTT Version 0.0.4 (2009-02-22)
|
4
|
+
|
5
|
+
Re-factored packet encoding/decoding into one class per packet type.
|
6
|
+
Added MQTT::Proxy class for implementing an MQTT proxy.
|
7
|
+
|
3
8
|
== Ruby MQTT Version 0.0.3 (2009-02-08)
|
4
9
|
|
5
10
|
Added checking of Connection Acknowledgement.
|
data/README
CHANGED
@@ -13,7 +13,7 @@ You may get the latest stable version from Rubyforge. Source gems are also avail
|
|
13
13
|
== Synopsis
|
14
14
|
|
15
15
|
require 'rubygems'
|
16
|
-
require 'mqtt'
|
16
|
+
require 'mqtt/client'
|
17
17
|
|
18
18
|
# Publish example
|
19
19
|
mqtt = MQTT::Client.new('mqtt.example.com')
|
@@ -34,16 +34,17 @@ You may get the latest stable version from Rubyforge. Source gems are also avail
|
|
34
34
|
|
35
35
|
== TODO
|
36
36
|
|
37
|
-
* Process acknowledgement packets
|
38
|
-
* Create classes for each type of packet?
|
39
|
-
* More validations of data/parameters
|
40
|
-
* Implement exception throwing
|
41
37
|
* Implement Will and Testament
|
38
|
+
* Process acknowledgement packets / Implement QOS 1 in client
|
39
|
+
* More validations of data/parameters
|
40
|
+
* More error checking and exception throwing
|
41
|
+
- Check that packet data is valid - don't blindly read values
|
42
|
+
- Subscribe and Unsubscribe packets should always have QOS of 1
|
42
43
|
* More examples
|
43
44
|
* Integration tests
|
44
45
|
* Refactor to add callbacks that are called from seperate thread
|
45
|
-
* Implement QOS Level
|
46
|
-
*
|
46
|
+
* Implement QOS Level 2 in client
|
47
|
+
* Prevent proxy from connecting to itself
|
47
48
|
* Add support for binding socket to specific local address
|
48
49
|
|
49
50
|
== Resources
|
data/Rakefile
CHANGED
data/examples/livetext.rb
CHANGED
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift File.dirname(__FILE__)+'/../lib'
|
4
|
+
|
5
|
+
require 'mqtt/proxy'
|
6
|
+
|
7
|
+
proxy = MQTT::Proxy.new(
|
8
|
+
:local_host => '0.0.0.0',
|
9
|
+
:local_port => 1883,
|
10
|
+
:broker_host => 'mqtt.example.com',
|
11
|
+
:broker_port => 1883
|
12
|
+
)
|
13
|
+
|
14
|
+
proxy.client_filter = lambda { |packet|
|
15
|
+
puts "From client: #{packet.inspect}"
|
16
|
+
return packet
|
17
|
+
}
|
18
|
+
|
19
|
+
proxy.broker_filter = lambda { |packet|
|
20
|
+
puts "From broker: #{packet.inspect}"
|
21
|
+
return packet
|
22
|
+
}
|
23
|
+
|
24
|
+
# Start the proxy
|
25
|
+
proxy.run
|
data/examples/simple_get.rb
CHANGED
data/examples/simple_publish.rb
CHANGED
data/lib/mqtt.rb
CHANGED
@@ -1,29 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require 'mqtt/client'
|
4
|
-
|
5
3
|
# Pure-ruby implementation of the MQTT protocol
|
6
4
|
module MQTT
|
7
5
|
|
8
|
-
PACKET_TYPES = [
|
9
|
-
nil,
|
10
|
-
:connect, # Client request to connect to Broker
|
11
|
-
:connack, # Connect Acknowledgment
|
12
|
-
:publish, # Publish message
|
13
|
-
:puback, # Publish Acknowledgment
|
14
|
-
:pubrec, # Publish Received (assured delivery part 1)
|
15
|
-
:pubrel, # Publish Release (assured delivery part 2)
|
16
|
-
:pubcomp, # Publish Complete (assured delivery part 3)
|
17
|
-
:subscribe, # Client Subscribe request
|
18
|
-
:suback, # Subscribe Acknowledgment
|
19
|
-
:unsubscribe, # Client Unsubscribe request
|
20
|
-
:unsuback, # Unsubscribe Acknowledgment
|
21
|
-
:pingreq, # PING Request
|
22
|
-
:pingresp, # PING Response
|
23
|
-
:disconnect, # Client is Disconnecting
|
24
|
-
nil
|
25
|
-
]
|
26
|
-
|
27
6
|
class Exception < Exception
|
28
7
|
|
29
8
|
end
|
data/lib/mqtt/client.rb
CHANGED
@@ -48,22 +48,12 @@ module MQTT
|
|
48
48
|
@socket = TCPSocket.new(@remote_host,@remote_port)
|
49
49
|
|
50
50
|
# Protocol name and version
|
51
|
-
packet = MQTT::Packet.new(
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
connect_flags ||= 0x02 if @clean_start
|
58
|
-
# FIXME: implement Will and Testament
|
59
|
-
packet.add_bytes(connect_flags)
|
60
|
-
|
61
|
-
# Keep Alive timer: 10 seconds
|
62
|
-
packet.add_short(@keep_alive)
|
63
|
-
|
64
|
-
# Add the client identifier
|
65
|
-
packet.add_string(@client_id)
|
66
|
-
|
51
|
+
packet = MQTT::Packet::Connect.new(
|
52
|
+
:clean_start => @clean_start,
|
53
|
+
:keep_alive => @keep_alive,
|
54
|
+
:client_id => @client_id
|
55
|
+
)
|
56
|
+
|
67
57
|
# Send packet
|
68
58
|
send_packet(packet)
|
69
59
|
|
@@ -89,7 +79,7 @@ module MQTT
|
|
89
79
|
def disconnect(send_msg=true)
|
90
80
|
if connected?
|
91
81
|
if send_msg
|
92
|
-
packet = MQTT::Packet.new
|
82
|
+
packet = MQTT::Packet::Disconnect.new
|
93
83
|
send_packet(packet)
|
94
84
|
end
|
95
85
|
@read_thread.kill if @read_thread and @read_thread.alive?
|
@@ -106,30 +96,21 @@ module MQTT
|
|
106
96
|
|
107
97
|
# Send a MQTT ping message to indicate that the MQTT client is alive.
|
108
98
|
def ping
|
109
|
-
packet = MQTT::Packet.new
|
99
|
+
packet = MQTT::Packet::Pingreq.new
|
110
100
|
send_packet(packet)
|
111
101
|
@last_pingreq = Time.now
|
112
102
|
end
|
113
103
|
|
114
104
|
# Publish a message on a particular topic to the MQTT broker.
|
115
105
|
def publish(topic, payload, retain=false, qos=0)
|
116
|
-
packet = MQTT::Packet.new(
|
117
|
-
:type => :publish,
|
106
|
+
packet = MQTT::Packet::Publish.new(
|
118
107
|
:qos => qos,
|
119
|
-
:retain => retain
|
108
|
+
:retain => retain,
|
109
|
+
:topic => topic,
|
110
|
+
:payload => payload,
|
111
|
+
:message_id => @message_id.next
|
120
112
|
)
|
121
|
-
|
122
|
-
# Add the topic name
|
123
|
-
packet.add_string(topic)
|
124
|
-
|
125
|
-
# Add Message ID for qos1 and qos2
|
126
|
-
unless qos == 0
|
127
|
-
packet.add_short(@message_id.next)
|
128
|
-
end
|
129
|
-
|
130
|
-
# Add the packet payload
|
131
|
-
packet.add_data(payload)
|
132
|
-
|
113
|
+
|
133
114
|
# Send the packet
|
134
115
|
send_packet(packet)
|
135
116
|
end
|
@@ -147,27 +128,10 @@ module MQTT
|
|
147
128
|
# client.subscribe( 'a/b' => 0, 'c/d' => 1 )
|
148
129
|
#
|
149
130
|
def subscribe(*topics)
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
array += item.sort
|
155
|
-
elsif item.is_a?(Array)
|
156
|
-
# Already in [topic,qos] format
|
157
|
-
array.push item
|
158
|
-
else
|
159
|
-
# Default to QOS 0
|
160
|
-
array.push [item.to_s,0]
|
161
|
-
end
|
162
|
-
end
|
163
|
-
|
164
|
-
# Create the packet
|
165
|
-
packet = MQTT::Packet.new(:type => :subscribe, :qos => 1)
|
166
|
-
packet.add_short(@message_id.next)
|
167
|
-
array.each do |item|
|
168
|
-
packet.add_string(item[0])
|
169
|
-
packet.add_bytes(item[1])
|
170
|
-
end
|
131
|
+
packet = MQTT::Packet::Subscribe.new(
|
132
|
+
:topics => topics,
|
133
|
+
:message_id => @message_id.next
|
134
|
+
)
|
171
135
|
send_packet(packet)
|
172
136
|
end
|
173
137
|
|
@@ -180,17 +144,17 @@ module MQTT
|
|
180
144
|
def get
|
181
145
|
# Wait for a packet to be available
|
182
146
|
packet = @read_queue.pop
|
183
|
-
|
184
|
-
|
185
|
-
topic
|
186
|
-
msg_id = packet.shift_short unless (packet.qos == 0)
|
187
|
-
return topic,packet.body
|
147
|
+
topic = packet.topic
|
148
|
+
payload = packet.payload
|
149
|
+
return topic,payload
|
188
150
|
end
|
189
151
|
|
190
152
|
# Send a unsubscribe message for one or more topics on the MQTT broker
|
191
153
|
def unsubscribe(*topics)
|
192
|
-
packet = MQTT::Packet.new(
|
193
|
-
|
154
|
+
packet = MQTT::Packet::Unsubscribe.new(
|
155
|
+
:topics => topics,
|
156
|
+
:message_id => @message_id.next
|
157
|
+
)
|
194
158
|
send_packet(packet)
|
195
159
|
end
|
196
160
|
|
@@ -205,7 +169,7 @@ module MQTT
|
|
205
169
|
unless result.nil?
|
206
170
|
# Yes - read in the packet
|
207
171
|
packet = MQTT::Packet.read(@socket)
|
208
|
-
if packet.
|
172
|
+
if packet.class == MQTT::Packet::Publish
|
209
173
|
# Add to queue
|
210
174
|
@read_queue.push(packet)
|
211
175
|
else
|
@@ -236,23 +200,13 @@ module MQTT
|
|
236
200
|
def receive_connack
|
237
201
|
Timeout.timeout(@ack_timeout) do
|
238
202
|
packet = MQTT::Packet.read(@socket)
|
239
|
-
if packet.
|
240
|
-
raise MQTT::ProtocolException.new("Response wan't a connection acknowledgement: #{packet.
|
203
|
+
if packet.class != MQTT::Packet::Connack
|
204
|
+
raise MQTT::ProtocolException.new("Response wan't a connection acknowledgement: #{packet.class}")
|
241
205
|
end
|
242
206
|
|
243
|
-
#
|
244
|
-
|
245
|
-
|
246
|
-
# Success
|
247
|
-
nil
|
248
|
-
elsif return_code == 0x01
|
249
|
-
raise MQTT::ProtocolException.new("Connection refused: unacceptable protocol version")
|
250
|
-
elsif return_code == 0x02
|
251
|
-
raise MQTT::ProtocolException.new("Connection refused: client identifier rejected")
|
252
|
-
elsif return_code == 0x03
|
253
|
-
raise MQTT::ProtocolException.new("Connection refused: broker unavailable")
|
254
|
-
else
|
255
|
-
raise MQTT::ProtocolException.new("Connection refused: #{return_code}")
|
207
|
+
# Check the return code
|
208
|
+
if packet.return_code != 0x00
|
209
|
+
raise MQTT::ProtocolException.new(packet.return_msg)
|
256
210
|
end
|
257
211
|
end
|
258
212
|
end
|
data/lib/mqtt/packet.rb
CHANGED
@@ -7,19 +7,19 @@ module MQTT
|
|
7
7
|
# Class representing a MQTT Packet
|
8
8
|
# Performs binary encoding and decoding of headers
|
9
9
|
class Packet
|
10
|
-
attr_reader :type # The packet type
|
11
10
|
attr_reader :dup # Duplicate delivery flag
|
12
11
|
attr_reader :retain # Retain flag
|
13
12
|
attr_reader :qos # Quality of Service level
|
14
|
-
attr_reader :body # Packet's body (everything after fixed header)
|
15
13
|
|
16
14
|
# Read in a packet from a socket
|
17
15
|
def self.read(socket)
|
18
|
-
|
19
|
-
# Create a packet object
|
16
|
+
# Read in the packet header and work out the class
|
20
17
|
header = read_byte(socket)
|
21
|
-
|
22
|
-
|
18
|
+
type_id = ((header & 0xF0) >> 4)
|
19
|
+
packet_class = MQTT::PACKET_TYPES[type_id]
|
20
|
+
|
21
|
+
# Create a new packet object
|
22
|
+
packet = packet_class.new(
|
23
23
|
:dup => ((header & 0x08) >> 3),
|
24
24
|
:qos => ((header & 0x06) >> 1),
|
25
25
|
:retain => ((header & 0x01) >> 0)
|
@@ -36,39 +36,22 @@ module MQTT
|
|
36
36
|
# FIXME: only allow 4 bytes?
|
37
37
|
|
38
38
|
# Read in the packet body
|
39
|
-
packet.
|
39
|
+
packet.parse_body( socket.read(body_len) )
|
40
40
|
|
41
41
|
return packet
|
42
42
|
end
|
43
43
|
|
44
44
|
# Create a new empty packet
|
45
45
|
def initialize(args={})
|
46
|
-
self.type = args[:type] || :invalid
|
47
46
|
self.dup = args[:dup] || false
|
48
47
|
self.qos = args[:qos] || 0
|
49
48
|
self.retain = args[:retain] || false
|
50
|
-
self.body = args[:body] || ''
|
51
49
|
end
|
52
50
|
|
53
|
-
#
|
54
|
-
# Can either by the packet type id (integer)
|
55
|
-
# Or the packet type as a symbol/string
|
56
|
-
# See the MQTT module for an enumeration of packet types.
|
57
|
-
def type=(arg)
|
58
|
-
if arg.kind_of?(Integer)
|
59
|
-
# Convert type identifier to symbol
|
60
|
-
@type = MQTT::PACKET_TYPES[arg]
|
61
|
-
else
|
62
|
-
@type = arg.to_sym
|
63
|
-
# FIXME: raise exception if packet type is invalid?
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
# Return the identifer for this packet type
|
51
|
+
# Get the identifer for this packet type
|
68
52
|
def type_id
|
69
|
-
|
70
|
-
|
71
|
-
raise "Invalid packet type: #{@type}" if index.nil?
|
53
|
+
index = MQTT::PACKET_TYPES.index(self.class)
|
54
|
+
raise "Invalid packet type: #{self.class}" if index.nil?
|
72
55
|
return index
|
73
56
|
end
|
74
57
|
|
@@ -94,62 +77,17 @@ module MQTT
|
|
94
77
|
def qos=(arg)
|
95
78
|
@qos = arg.to_i
|
96
79
|
end
|
97
|
-
|
98
|
-
# Set (replace) the packet body
|
99
|
-
def body=(arg)
|
100
|
-
# FIXME: only allow 268435455 bytes?
|
101
|
-
@body = arg.to_s
|
102
|
-
end
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
# Add an array of bytes to the end of the packet's body
|
109
|
-
def add_bytes(*bytes)
|
110
|
-
@body += bytes.pack('C*')
|
111
|
-
end
|
112
80
|
|
113
|
-
#
|
114
|
-
def
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
# Add some raw data to the end of the packet's body
|
119
|
-
def add_data(data)
|
120
|
-
data = data.to_s unless data.is_a?(String)
|
121
|
-
@body += data
|
122
|
-
end
|
123
|
-
|
124
|
-
# Add a string to the end of the packet's body
|
125
|
-
# (preceded by the length of the string)
|
126
|
-
def add_string(str)
|
127
|
-
str = str.to_s unless str.is_a?(String)
|
128
|
-
add_short(str.size)
|
129
|
-
add_data(str)
|
130
|
-
end
|
131
|
-
|
132
|
-
|
133
|
-
# Remove a 16-bit unsigned integer from the front on the body
|
134
|
-
def shift_short
|
135
|
-
bytes = @body.slice!(0..1)
|
136
|
-
bytes.unpack('n').first
|
137
|
-
end
|
138
|
-
|
139
|
-
# Remove n bytes from the front on the body
|
140
|
-
def shift_bytes(bytes)
|
141
|
-
@body.slice!(0...bytes).unpack('C*')
|
142
|
-
end
|
143
|
-
|
144
|
-
# Remove n bytes from the front on the body
|
145
|
-
def shift_data(bytes)
|
146
|
-
@body.slice!(0...bytes)
|
81
|
+
# Parse the body (variable header and payload) of a packet
|
82
|
+
def parse_body(buffer)
|
83
|
+
unless buffer.size == 0
|
84
|
+
raise MQTT::ProtocolException.new("Error: parse_body was not sub-classed for a packet with a payload")
|
85
|
+
end
|
147
86
|
end
|
148
87
|
|
149
|
-
#
|
150
|
-
def
|
151
|
-
|
152
|
-
shift_data(len)
|
88
|
+
# Get serialisation of packet's body (variable header and payload)
|
89
|
+
def encode_body
|
90
|
+
'' # No body by default
|
153
91
|
end
|
154
92
|
|
155
93
|
|
@@ -163,8 +101,11 @@ module MQTT
|
|
163
101
|
(retain ? 0x1 : 0x0)
|
164
102
|
]
|
165
103
|
|
104
|
+
# Get the packet's variable header and payload
|
105
|
+
body = self.encode_body
|
106
|
+
|
166
107
|
# Build up the body length field bytes
|
167
|
-
body_size =
|
108
|
+
body_size = body.size
|
168
109
|
begin
|
169
110
|
digit = (body_size % 128)
|
170
111
|
body_size = (body_size / 128)
|
@@ -174,24 +115,493 @@ module MQTT
|
|
174
115
|
end while (body_size > 0)
|
175
116
|
|
176
117
|
# Convert header to binary and add on body
|
177
|
-
header.pack('C*') +
|
118
|
+
header.pack('C*') + body
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
protected
|
123
|
+
|
124
|
+
# Encode an array of bytes and return them
|
125
|
+
def encode_bytes(*bytes)
|
126
|
+
bytes.pack('C*')
|
127
|
+
end
|
128
|
+
|
129
|
+
# Encode a 16-bit unsigned integer and return it
|
130
|
+
def encode_short(val)
|
131
|
+
[val.to_i].pack('n')
|
132
|
+
end
|
133
|
+
|
134
|
+
# Encode a string and return it
|
135
|
+
# (preceded by the length of the string)
|
136
|
+
def encode_string(str)
|
137
|
+
str = str.to_s unless str.is_a?(String)
|
138
|
+
encode_short(str.size) + str
|
139
|
+
end
|
140
|
+
|
141
|
+
# Remove a 16-bit unsigned integer from the front of buffer
|
142
|
+
def shift_short(buffer)
|
143
|
+
bytes = buffer.slice!(0..1)
|
144
|
+
bytes.unpack('n').first
|
178
145
|
end
|
179
146
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
"qos=#{@qos}, body.size=#{@body.size}>"
|
147
|
+
# Remove one byte from the front of the string
|
148
|
+
def shift_byte(buffer)
|
149
|
+
buffer.slice!(0...1).unpack('C').first
|
184
150
|
end
|
185
151
|
|
152
|
+
# Remove n bytes from the front of buffer
|
153
|
+
def shift_data(buffer,bytes)
|
154
|
+
buffer.slice!(0...bytes)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Remove string from the front of buffer
|
158
|
+
def shift_string(buffer)
|
159
|
+
len = shift_short(buffer)
|
160
|
+
shift_data(buffer,len)
|
161
|
+
end
|
162
|
+
|
163
|
+
|
186
164
|
private
|
187
165
|
|
188
|
-
# Read and unpack a single byte from socket
|
166
|
+
# Read and unpack a single byte from a socket
|
189
167
|
def self.read_byte(socket)
|
190
168
|
byte = socket.read(1)
|
191
169
|
raise MQTT::ProtocolException if byte.nil?
|
192
170
|
byte.unpack('C').first
|
193
171
|
end
|
172
|
+
|
173
|
+
|
174
|
+
|
175
|
+
## PACKET SUBCLASSES ##
|
176
|
+
|
177
|
+
|
178
|
+
# Class representing an MQTT Publish message
|
179
|
+
class Publish < MQTT::Packet
|
180
|
+
attr_accessor :topic
|
181
|
+
attr_accessor :message_id
|
182
|
+
attr_accessor :payload
|
183
|
+
|
184
|
+
# Create a new Unsubscribe Acknowledgment packet
|
185
|
+
def initialize(args={})
|
186
|
+
super(args)
|
187
|
+
self.topic = args[:topic] || nil
|
188
|
+
self.message_id = args[:message_id] || 0
|
189
|
+
self.payload = args[:payload] || ''
|
190
|
+
end
|
191
|
+
|
192
|
+
# Get serialisation of packet's body
|
193
|
+
def encode_body
|
194
|
+
body = ''
|
195
|
+
raise "Invalid topic name when serialising packet" if @topic.nil?
|
196
|
+
body += encode_string(@topic)
|
197
|
+
body += encode_short(@message_id) unless qos == 0
|
198
|
+
body += payload.to_s
|
199
|
+
return body
|
200
|
+
end
|
201
|
+
|
202
|
+
# Parse the body (variable header and payload) of a Publish packet
|
203
|
+
def parse_body(buffer)
|
204
|
+
@topic = shift_string(buffer)
|
205
|
+
@message_id = shift_short(buffer) unless qos == 0
|
206
|
+
@payload = buffer.dup
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Class representing an MQTT Connect Packet
|
211
|
+
class Connect < MQTT::Packet
|
212
|
+
attr_accessor :protocol_name
|
213
|
+
attr_accessor :protocol_version
|
214
|
+
attr_accessor :client_id
|
215
|
+
attr_accessor :clean_start
|
216
|
+
attr_accessor :keep_alive
|
217
|
+
attr_accessor :will_topic
|
218
|
+
attr_accessor :will_qos
|
219
|
+
attr_accessor :will_retain
|
220
|
+
attr_accessor :will_payload
|
221
|
+
|
222
|
+
# Create a new Client Connect packet
|
223
|
+
def initialize(args={})
|
224
|
+
super(args)
|
225
|
+
self.protocol_name = args[:protocol_name] || 'MQIsdp'
|
226
|
+
self.protocol_version = args[:protocol_version] || 0x03
|
227
|
+
self.client_id = args[:client_id] || nil
|
228
|
+
self.clean_start = args[:clean_start] || true
|
229
|
+
self.keep_alive = args[:keep_alive] || 10
|
230
|
+
self.will_topic = args[:will_topic] || nil
|
231
|
+
self.will_qos = args[:will_qos] || 0
|
232
|
+
self.will_retain = args[:will_retain] || false
|
233
|
+
self.will_payload = args[:will_payload] || ''
|
234
|
+
end
|
235
|
+
|
236
|
+
# Get serialisation of packet's body
|
237
|
+
def encode_body
|
238
|
+
body = ''
|
239
|
+
raise "Invalid client identifier when serialising packet" if @client_id.nil?
|
240
|
+
body += encode_string(@protocol_name)
|
241
|
+
body += encode_bytes(@protocol_version.to_i)
|
242
|
+
body += encode_bytes(0) # Connect Flags
|
243
|
+
body += encode_short(@keep_alive) # Keep Alive timer
|
244
|
+
body += encode_string(@client_id)
|
245
|
+
# FIXME: implement Will
|
246
|
+
#unless @will_topic.nil?
|
247
|
+
# body += encode_string(@will_topic)
|
248
|
+
# body += will_payload.to_s
|
249
|
+
#end
|
250
|
+
return body
|
251
|
+
end
|
252
|
+
|
253
|
+
# Parse the body (variable header and payload) of a Connect packet
|
254
|
+
def parse_body(buffer)
|
255
|
+
@protocol_name = shift_string(buffer)
|
256
|
+
@protocol_version = shift_byte(buffer)
|
257
|
+
flags = shift_byte(buffer)
|
258
|
+
@keep_alive = shift_short(buffer)
|
259
|
+
@client_id = shift_string(buffer)
|
260
|
+
# FIXME: implement Will
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
# Class representing an MQTT Connect Acknowledgment Packet
|
265
|
+
class Connack < MQTT::Packet
|
266
|
+
attr_accessor :return_code
|
267
|
+
|
268
|
+
# Create a new Client Connect packet
|
269
|
+
def initialize(args={})
|
270
|
+
super(args)
|
271
|
+
self.return_code = args[:return_code] || 0
|
272
|
+
end
|
273
|
+
|
274
|
+
# Get a string message corresponding to a return code
|
275
|
+
def return_msg
|
276
|
+
case return_code
|
277
|
+
when 0x00
|
278
|
+
"Connection Accepted"
|
279
|
+
when 0x01
|
280
|
+
"Connection refused: unacceptable protocol version"
|
281
|
+
when 0x02
|
282
|
+
"Connection refused: client identifier rejected"
|
283
|
+
when 0x03
|
284
|
+
"Connection refused: broker unavailable"
|
285
|
+
else
|
286
|
+
"Connection refused: error code #{return_code}"
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# Get serialisation of packet's body
|
291
|
+
def encode_body
|
292
|
+
body = ''
|
293
|
+
body += encode_bytes(0) # Unused
|
294
|
+
body += encode_bytes(@return_code.to_i) # Return Code
|
295
|
+
return body
|
296
|
+
end
|
297
|
+
|
298
|
+
# Parse the body (variable header and payload) of a Connect Acknowledgment packet
|
299
|
+
def parse_body(buffer)
|
300
|
+
unused = shift_byte(buffer)
|
301
|
+
@return_code = shift_byte(buffer)
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
# Class representing an MQTT Publish Acknowledgment packet
|
306
|
+
class Puback < MQTT::Packet
|
307
|
+
attr_accessor :message_id
|
308
|
+
|
309
|
+
# Create a new Unsubscribe Acknowledgment packet
|
310
|
+
def initialize(args={})
|
311
|
+
super(args)
|
312
|
+
self.message_id = args[:message_id] || 0
|
313
|
+
end
|
314
|
+
|
315
|
+
# Get serialisation of packet's body
|
316
|
+
def encode_body
|
317
|
+
encode_short(@message_id)
|
318
|
+
end
|
319
|
+
|
320
|
+
# Parse the body (variable header and payload) of a packet
|
321
|
+
def parse_body(buffer)
|
322
|
+
@message_id = shift_short(buffer)
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
# Class representing an MQTT Publish Received packet
|
327
|
+
class Pubrec < MQTT::Packet
|
328
|
+
attr_accessor :message_id
|
329
|
+
|
330
|
+
# Create a new Unsubscribe Acknowledgment packet
|
331
|
+
def initialize(args={})
|
332
|
+
super(args)
|
333
|
+
self.message_id = args[:message_id] || 0
|
334
|
+
end
|
335
|
+
|
336
|
+
# Get serialisation of packet's body
|
337
|
+
def encode_body
|
338
|
+
encode_short(@message_id)
|
339
|
+
end
|
340
|
+
|
341
|
+
# Parse the body (variable header and payload) of a packet
|
342
|
+
def parse_body(buffer)
|
343
|
+
@message_id = shift_short(buffer)
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
# Class representing an MQTT Publish Release packet
|
348
|
+
class Pubrel < MQTT::Packet
|
349
|
+
attr_accessor :message_id
|
350
|
+
|
351
|
+
# Create a new Unsubscribe Acknowledgment packet
|
352
|
+
def initialize(args={})
|
353
|
+
super(args)
|
354
|
+
self.message_id = args[:message_id] || 0
|
355
|
+
end
|
356
|
+
|
357
|
+
# Get serialisation of packet's body
|
358
|
+
def encode_body
|
359
|
+
encode_short(@message_id)
|
360
|
+
end
|
361
|
+
|
362
|
+
# Parse the body (variable header and payload) of a packet
|
363
|
+
def parse_body(buffer)
|
364
|
+
@message_id = shift_short(buffer)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
# Class representing an MQTT Publish Complete packet
|
369
|
+
class Pubcomp < MQTT::Packet
|
370
|
+
attr_accessor :message_id
|
371
|
+
|
372
|
+
# Create a new Unsubscribe Acknowledgment packet
|
373
|
+
def initialize(args={})
|
374
|
+
super(args)
|
375
|
+
self.message_id = args[:message_id] || 0
|
376
|
+
end
|
377
|
+
|
378
|
+
# Get serialisation of packet's body
|
379
|
+
def encode_body
|
380
|
+
encode_short(@message_id)
|
381
|
+
end
|
382
|
+
|
383
|
+
# Parse the body (variable header and payload) of a packet
|
384
|
+
def parse_body(buffer)
|
385
|
+
@message_id = shift_short(buffer)
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
# Class representing an MQTT Client Subscribe packet
|
390
|
+
class Subscribe < MQTT::Packet
|
391
|
+
attr_reader :topics
|
392
|
+
attr_accessor :message_id
|
393
|
+
|
394
|
+
# Create a new Unsubscribe Acknowledgment packet
|
395
|
+
def initialize(args={})
|
396
|
+
super(args)
|
397
|
+
self.topics = args[:topics] || []
|
398
|
+
self.message_id = args[:message_id] || 0
|
399
|
+
self.qos = 1 # Force a QOS of 1
|
400
|
+
end
|
401
|
+
|
402
|
+
# Set one or more topics for the Subscrible packet
|
403
|
+
# The topics parameter should be one of the following:
|
404
|
+
# * String: subscribe to one topic with QOS 0
|
405
|
+
# * Array: subscribe to multiple topics with QOS 0
|
406
|
+
# * Hash: subscribe to multiple topics where the key is the topic and the value is the QOS level
|
407
|
+
#
|
408
|
+
# For example:
|
409
|
+
# packet.topics = 'a/b'
|
410
|
+
# packet.topics = ['a/b', 'c/d']
|
411
|
+
# packet.topics = [['a/b',0], ['c/d',1]]
|
412
|
+
# packet.topics = {'a/b' => 0, 'c/d' => 1}
|
413
|
+
#
|
414
|
+
def topics=(value)
|
415
|
+
# Get input into a consistent state
|
416
|
+
if value.is_a?(Array)
|
417
|
+
input = value.flatten
|
418
|
+
else
|
419
|
+
input = [value]
|
420
|
+
end
|
421
|
+
|
422
|
+
@topics = []
|
423
|
+
while(input.size>0)
|
424
|
+
item = input.shift
|
425
|
+
if item.is_a?(Hash)
|
426
|
+
# Convert hash into an ordered array of arrays
|
427
|
+
@topics += item.sort
|
428
|
+
elsif item.is_a?(String)
|
429
|
+
# Peek at the next item in the array, and remove it if it is an integer
|
430
|
+
if input.first.is_a?(Integer)
|
431
|
+
qos = input.shift
|
432
|
+
@topics << [item,qos]
|
433
|
+
else
|
434
|
+
@topics << [item,0]
|
435
|
+
end
|
436
|
+
else
|
437
|
+
# Meh?
|
438
|
+
raise "Invalid topics input: #{value.inspect}"
|
439
|
+
end
|
440
|
+
end
|
441
|
+
@topics
|
442
|
+
end
|
443
|
+
|
444
|
+
# Get serialisation of packet's body
|
445
|
+
def encode_body
|
446
|
+
raise "no topics given when serialising packet" if @topics.empty?
|
447
|
+
body = encode_short(@message_id)
|
448
|
+
topics.each do |item|
|
449
|
+
body += encode_string(item[0])
|
450
|
+
body += encode_bytes(item[1])
|
451
|
+
end
|
452
|
+
return body
|
453
|
+
end
|
454
|
+
|
455
|
+
# Parse the body (variable header and payload) of a packet
|
456
|
+
def parse_body(buffer)
|
457
|
+
@message_id = shift_short(buffer)
|
458
|
+
@topics = []
|
459
|
+
while(buffer.size>0)
|
460
|
+
topic_name = shift_string(buffer)
|
461
|
+
topic_qos = shift_byte(buffer)
|
462
|
+
@topics << [topic_name,topic_qos]
|
463
|
+
end
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
# Class representing an MQTT Subscribe Acknowledgment packet
|
468
|
+
class Suback < MQTT::Packet
|
469
|
+
attr_accessor :message_id
|
470
|
+
attr_reader :granted_qos
|
471
|
+
|
472
|
+
# Create a new Unsubscribe Acknowledgment packet
|
473
|
+
def initialize(args={})
|
474
|
+
super(args)
|
475
|
+
self.message_id = args[:message_id] || 0
|
476
|
+
self.granted_qos = args[:granted_qos] || []
|
477
|
+
end
|
478
|
+
|
479
|
+
def granted_qos=(value)
|
480
|
+
raise "granted QOS should be an array of arrays" unless value.is_a?(Array)
|
481
|
+
@granted_qos = value
|
482
|
+
end
|
483
|
+
|
484
|
+
# Get serialisation of packet's body
|
485
|
+
def encode_body
|
486
|
+
raise "no granted QOS given when serialising packet" if @granted_qos.empty?
|
487
|
+
body = encode_short(@message_id)
|
488
|
+
granted_qos.flatten.each { |qos| body += encode_bytes(qos) }
|
489
|
+
return body
|
490
|
+
end
|
491
|
+
|
492
|
+
# Parse the body (variable header and payload) of a packet
|
493
|
+
def parse_body(buffer)
|
494
|
+
@message_id = shift_short(buffer)
|
495
|
+
while(buffer.size>0)
|
496
|
+
@granted_qos << [shift_byte(buffer),shift_byte(buffer)]
|
497
|
+
end
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
# Class representing an MQTT Client Unsubscribe packet
|
502
|
+
class Unsubscribe < MQTT::Packet
|
503
|
+
attr_reader :topics
|
504
|
+
attr_accessor :message_id
|
505
|
+
|
506
|
+
# Create a new Unsubscribe Acknowledgment packet
|
507
|
+
def initialize(args={})
|
508
|
+
super(args)
|
509
|
+
self.topics = args[:topics] || []
|
510
|
+
self.message_id = args[:message_id] || 0
|
511
|
+
self.qos = 1 # Force a QOS of 1
|
512
|
+
end
|
513
|
+
|
514
|
+
def topics=(value)
|
515
|
+
if value.is_a?(Array)
|
516
|
+
@topics = value
|
517
|
+
else
|
518
|
+
@topics = [value]
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
# Get serialisation of packet's body
|
523
|
+
def encode_body
|
524
|
+
raise "no topics given when serialising packet" if @topics.empty?
|
525
|
+
body = encode_short(@message_id)
|
526
|
+
topics.each { |topic| body += encode_string(topic) }
|
527
|
+
return body
|
528
|
+
end
|
529
|
+
|
530
|
+
# Parse the body (variable header and payload) of a packet
|
531
|
+
def parse_body(buffer)
|
532
|
+
@message_id = shift_short(buffer)
|
533
|
+
while(buffer.size>0)
|
534
|
+
@topics << shift_string(buffer)
|
535
|
+
end
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
# Class representing an MQTT Unsubscribe Acknowledgment packet
|
540
|
+
class Unsuback < MQTT::Packet
|
541
|
+
attr_accessor :message_id
|
542
|
+
|
543
|
+
# Create a new Unsubscribe Acknowledgment packet
|
544
|
+
def initialize(args={})
|
545
|
+
super(args)
|
546
|
+
self.message_id = args[:message_id] || 0
|
547
|
+
end
|
548
|
+
|
549
|
+
# Get serialisation of packet's body
|
550
|
+
def encode_body
|
551
|
+
encode_short(@message_id)
|
552
|
+
end
|
553
|
+
|
554
|
+
# Parse the body (variable header and payload) of a packet
|
555
|
+
def parse_body(buffer)
|
556
|
+
@message_id = shift_short(buffer)
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|
560
|
+
# Class representing an MQTT Ping Request packet
|
561
|
+
class Pingreq < MQTT::Packet
|
562
|
+
# Create a new Ping Request packet
|
563
|
+
def initialize(args={})
|
564
|
+
super(args)
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
# Class representing an MQTT Ping Response packet
|
569
|
+
class Pingresp < MQTT::Packet
|
570
|
+
# Create a new Ping Response packet
|
571
|
+
def initialize(args={})
|
572
|
+
super(args)
|
573
|
+
end
|
574
|
+
end
|
575
|
+
|
576
|
+
# Class representing an MQTT Client Disconnect packet
|
577
|
+
class Disconnect < MQTT::Packet
|
578
|
+
# Create a new Client Disconnect packet
|
579
|
+
def initialize(args={})
|
580
|
+
super(args)
|
581
|
+
end
|
582
|
+
end
|
194
583
|
|
195
584
|
end
|
585
|
+
|
586
|
+
|
587
|
+
# An enumeration of the MQTT packet types
|
588
|
+
PACKET_TYPES = [
|
589
|
+
nil,
|
590
|
+
MQTT::Packet::Connect,
|
591
|
+
MQTT::Packet::Connack,
|
592
|
+
MQTT::Packet::Publish,
|
593
|
+
MQTT::Packet::Puback,
|
594
|
+
MQTT::Packet::Pubrec,
|
595
|
+
MQTT::Packet::Pubrel,
|
596
|
+
MQTT::Packet::Pubcomp,
|
597
|
+
MQTT::Packet::Subscribe,
|
598
|
+
MQTT::Packet::Suback,
|
599
|
+
MQTT::Packet::Unsubscribe,
|
600
|
+
MQTT::Packet::Unsuback,
|
601
|
+
MQTT::Packet::Pingreq,
|
602
|
+
MQTT::Packet::Pingresp,
|
603
|
+
MQTT::Packet::Disconnect,
|
604
|
+
nil
|
605
|
+
]
|
196
606
|
|
197
607
|
end
|
data/lib/mqtt/proxy.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'mqtt'
|
4
|
+
require 'mqtt/packet'
|
5
|
+
require 'thread'
|
6
|
+
require 'logger'
|
7
|
+
require 'socket'
|
8
|
+
|
9
|
+
|
10
|
+
module MQTT
|
11
|
+
|
12
|
+
# Class for implementing a proxy to filter/mangle MQTT packets.
|
13
|
+
class Proxy
|
14
|
+
attr_reader :local_host
|
15
|
+
attr_reader :local_port
|
16
|
+
attr_reader :broker_host
|
17
|
+
attr_reader :broker_port
|
18
|
+
attr_reader :listen_queue
|
19
|
+
attr_reader :select_timeout
|
20
|
+
attr_reader :logger
|
21
|
+
|
22
|
+
# Create a new MQTT Proxy instance.
|
23
|
+
#
|
24
|
+
# Possible argument keys:
|
25
|
+
#
|
26
|
+
# :local_host Address to bind listening socket to.
|
27
|
+
# :local_port Port to bind listening socket to.
|
28
|
+
# :broker_host Address of upstream broker to send packets upstream to.
|
29
|
+
# :broker_port Port of upstream broker to send packets upstream to.
|
30
|
+
# :select_timeout Time in seconds before disconnecting a connection.
|
31
|
+
# :logger Ruby Logger object to send informational messages to.
|
32
|
+
#
|
33
|
+
# NOTE: be careful not to connect to yourself!
|
34
|
+
def initialize(args={})
|
35
|
+
@local_host = args[:local_host] || '0.0.0.0'
|
36
|
+
@local_port = args[:local_port] || 1883
|
37
|
+
@broker_host = args[:broker_host] || 'localhost'
|
38
|
+
@broker_port = args[:broker_port] || 18830
|
39
|
+
@select_timeout = args[:select_timeout] || 60
|
40
|
+
|
41
|
+
# Setup a logger
|
42
|
+
@logger = args[:logger]
|
43
|
+
if @logger.nil?
|
44
|
+
@logger = Logger.new(STDOUT)
|
45
|
+
@logger.level = Logger::INFO
|
46
|
+
end
|
47
|
+
|
48
|
+
# Default is not to have any filters
|
49
|
+
@client_filter = nil
|
50
|
+
@broker_filter = nil
|
51
|
+
|
52
|
+
# Create TCP server socket
|
53
|
+
@server = TCPServer.open(@local_host,@local_port)
|
54
|
+
@logger.info "MQTT::Proxy listening on #{@local_host}:#{@local_port}"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Set a filter Proc for packets coming from the client (to the broker).
|
58
|
+
def client_filter=(proc)
|
59
|
+
@client_filter = proc
|
60
|
+
end
|
61
|
+
|
62
|
+
# Set a filter Proc for packets coming from the broker (to the client).
|
63
|
+
def broker_filter=(proc)
|
64
|
+
@broker_filter = proc
|
65
|
+
end
|
66
|
+
|
67
|
+
# Start accepting connections and processing packets.
|
68
|
+
def run
|
69
|
+
loop do
|
70
|
+
# Wait for a client to connect and then create a thread for it
|
71
|
+
Thread.new(@server.accept) do |client_socket|
|
72
|
+
logger.info "Accepted client: #{client_socket.peeraddr.join(':')}"
|
73
|
+
broker_socket = TCPSocket.new(@broker_host,@broker_port)
|
74
|
+
begin
|
75
|
+
process_packets(client_socket,broker_socket)
|
76
|
+
rescue Exception => exp
|
77
|
+
logger.error exp.to_s
|
78
|
+
end
|
79
|
+
logger.info "Disconnected: #{client_socket.peeraddr.join(':')}"
|
80
|
+
broker_socket.close
|
81
|
+
client_socket.close
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def process_packets(client_socket,broker_socket)
|
89
|
+
loop do
|
90
|
+
# Wait for some data on either socket
|
91
|
+
selected = IO.select([client_socket,broker_socket], nil, nil, @select_timeout)
|
92
|
+
if selected.nil?
|
93
|
+
# Timeout
|
94
|
+
raise "Timeout in select"
|
95
|
+
else
|
96
|
+
# Iterate through each of the sockets with data to read
|
97
|
+
if selected[0].include?(client_socket)
|
98
|
+
packet = MQTT::Packet.read(client_socket)
|
99
|
+
logger.debug "client -> <#{packet.type}>"
|
100
|
+
packet = @client_filter.call(packet) unless @client_filter.nil?
|
101
|
+
unless packet.nil?
|
102
|
+
broker_socket.write(packet)
|
103
|
+
logger.debug "<#{packet.type}> -> broker"
|
104
|
+
end
|
105
|
+
elsif selected[0].include?(broker_socket)
|
106
|
+
packet = MQTT::Packet.read(broker_socket)
|
107
|
+
logger.debug "broker -> <#{packet.type}>"
|
108
|
+
packet = @broker_filter.call(packet) unless @broker_filter.nil?
|
109
|
+
unless packet.nil?
|
110
|
+
client_socket.write(packet)
|
111
|
+
logger.debug "<#{packet.type}> -> client"
|
112
|
+
end
|
113
|
+
else
|
114
|
+
logger.error "Problem with select: socket is neither broker or client"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mqtt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nicholas J Humfrey
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-02-
|
12
|
+
date: 2009-02-22 00:00:00 +00:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -28,7 +28,9 @@ files:
|
|
28
28
|
- lib/mqtt.rb
|
29
29
|
- lib/mqtt/client.rb
|
30
30
|
- lib/mqtt/packet.rb
|
31
|
+
- lib/mqtt/proxy.rb
|
31
32
|
- examples/livetext.rb
|
33
|
+
- examples/readonly_proxy.rb
|
32
34
|
- examples/simple_get.rb
|
33
35
|
- examples/simple_publish.rb
|
34
36
|
- README
|