mini_mqtt 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 998e32192be7c070e3c79ffc234c874ecf3962e7
4
+ data.tar.gz: 426b36b3850393562047e1a09f9c1cf9067fdfb9
5
+ SHA512:
6
+ metadata.gz: 5e86d0852c429b21a11d035bbd0da3b1d8fc1c193ee7d106fe27787f75b931167a2ed7af46cec696b471c9e3bff09714c3a75f1d8d0617cc0f2717032a05093d
7
+ data.tar.gz: dbce45001fa0d8f7ef8b8a63c6a041c630722416f9cc67bd56add6641fbf54c34e24886000bea9e66f3ae852489540a8129b6c8987414d760390604522d0d0c0
data/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # MiniMqtt
2
+
3
+ A full ruby implementation of the client side of MQTT protocol.
4
+
5
+ The philosophy behind this gem is to keep the code as minimal and tidy as possible, and to completely avoid dependencies.
6
+
7
+ cloc lib
8
+
9
+ ---------------------------------------------------------------------
10
+ Language files blank comment code
11
+ ---------------------------------------------------------------------
12
+ Ruby 6 103 14 449
13
+ ---------------------------------------------------------------------
14
+
15
+ ## Features
16
+
17
+ * Supports all MQTT features up to QoS 1.
18
+ * Doesn't support websockets.
19
+ * Doesn't store messages once the connection has been closed.
20
+
21
+ ## Installation
22
+
23
+ Clone this repo and include it to your load path. This project isn't published at rubygems yet.
24
+
25
+ ## How to test
26
+ You need mosquitto server installed in order to run integration tests.
27
+
28
+ sudo apt-get install mosquitto
29
+
30
+ rake test
31
+
32
+ ## Usage
33
+
34
+ ```ruby
35
+ require 'mini_mqtt'
36
+ ```
37
+
38
+ ### Create client instance
39
+ Possible params are host, port, user, password, keep_alive (seconds), client_id, and clean_session
40
+ client_id defaults to random client id
41
+ clean_session defaults to true
42
+ keep_alive defaults to 10
43
+
44
+ ```ruby
45
+ client = MiniMqtt::Client.new host: 'test.mosquitto.org'
46
+ ```
47
+
48
+ ### Establish connection
49
+ Options are will_topic, will_message, will_retain(false) and will_qos(0)
50
+
51
+ ```ruby
52
+ client.connect
53
+ ```
54
+
55
+ You can check at any time if client is connected
56
+
57
+ ```ruby
58
+ puts client.connected?
59
+ ```
60
+
61
+ ### Publish messages
62
+
63
+ ```ruby
64
+ # Regular publish
65
+ client.publish '/topic', 'hello'
66
+
67
+ # Publish with retain
68
+ client.publish '/other_topic', 'retained_message', retain: true
69
+
70
+ # Publish with qos
71
+ client.publish '/qos_topic', 'message', qos: 1
72
+ ```
73
+
74
+ ### Subscribe to topics
75
+
76
+ ```ruby
77
+ # Single topic
78
+ client.subscribe '/topic'
79
+
80
+ # Multiple topics
81
+ client.subscribe '/topic', '/other_topic'
82
+
83
+ # Specifying max qos for topic (default 0)
84
+ client.subscribe '/topic', '/qos_topic' => 1
85
+ ```
86
+
87
+ ### Get messages
88
+ The caller of these methods are blocked until a message arrives, or the connection is lost.
89
+
90
+ ```ruby
91
+ # Get a single message
92
+ msg = client.get_message
93
+ puts msg.message, msg.topic, msg.qos, msg.retain, msg.packet_id, msg.dup
94
+
95
+ # Get messages in an infinite loop. Breaks if connection is lost.
96
+ client.get_messages do |msg, topic|
97
+ puts "Received #{ msg } on topic #{ topic }"
98
+ end
99
+ ```
100
+
101
+ ### Gracefully disconnect
102
+ client.disconnect
103
+ ```
@@ -0,0 +1,32 @@
1
+ module BinHelper
2
+ def uchar number
3
+ [number].pack 'C'
4
+ end
5
+
6
+ def ushort number
7
+ [number].pack 'n'
8
+ end
9
+
10
+ def flag_byte flags
11
+ raise "flags must have 8 elements" unless flags.size == 8
12
+ byte = 0
13
+ flags.reverse.each_with_index do |flag, index|
14
+ byte |= 1 << index if flag && flag != 0
15
+ end
16
+ byte
17
+ end
18
+
19
+ def mqtt_utf8_encode string
20
+ ushort(string.length) + string
21
+ end
22
+
23
+ def read_mqtt_encoded_string stream
24
+ length = read_ushort stream
25
+ stream.read length
26
+ end
27
+
28
+ def read_ushort stream
29
+ stream.read(2).unpack('n').first
30
+ end
31
+
32
+ end
@@ -0,0 +1,135 @@
1
+ require 'socket'
2
+
3
+ module MiniMqtt
4
+ class Client
5
+ attr_accessor :host, :port, :user, :password, :clean_session, :client_id
6
+
7
+ def initialize params = {}
8
+ @host = params[:host] || 'localhost'
9
+ @port = params[:port] || 1883
10
+ @user = params[:user]
11
+ @password = params[:password]
12
+ @keep_alive = params[:keep_alive] || 10
13
+ @client_id = params[:client_id] || generate_client_id
14
+ @clean_session = params.fetch :clean_session, true
15
+ end
16
+
17
+ def connect options = {}
18
+ # Create socket and packet handler
19
+ @socket = TCPSocket.new @host, @port
20
+ @packet_handler = PacketHandler.new @socket
21
+
22
+ # Send ConnectPacket
23
+ send_packet ConnectPacket.new user: @user,
24
+ password: @password, keep_alive: @keep_alive, client_id: @client_id,
25
+ clean_session: @clean_session, will_topic: options[:will_topic],
26
+ will_message: options[:will_message], will_retain: options[:will_retain]
27
+
28
+ # Receive connack packet
29
+ connack = receive_packet
30
+
31
+ if connack.accepted?
32
+ @received_messages = Queue.new
33
+ @last_ping_response = Time.now
34
+ spawn_read_thread!
35
+ spawn_keepalive_thread!
36
+ else
37
+ raise StandardError.new(connack.error)
38
+ end
39
+ end
40
+
41
+ def subscribe *params
42
+ # Each param can be a topic or a topic with its max qos.
43
+ # Example: subscribe 'topic1', 'topic2' => 1
44
+ topics = params.map do |arg|
45
+ arg.is_a?(Hash) ? arg : { arg => 0 }
46
+ end
47
+ topics = topics.inject :merge
48
+ packet = SubscribePacket.new topics: topics
49
+ send_packet packet
50
+ end
51
+
52
+ def unsubscribe *topics
53
+ send_packet UnsubscribePacket.new topics: topics
54
+ end
55
+
56
+ def publish topic, message, options = {}
57
+ packet = PublishPacket.new topic: topic, message: message.to_s,
58
+ retain: options[:retain], qos: options[:qos]
59
+ send_packet packet
60
+ end
61
+
62
+ def disconnect
63
+ # Send DisconnectPacket, then kill threads and close socket
64
+ send_packet DisconnectPacket.new
65
+ @read_thread.kill
66
+ @keepalive_thread.kill
67
+ @socket.close
68
+ end
69
+
70
+ def get_message
71
+ @received_messages.pop
72
+ end
73
+
74
+ def get_messages
75
+ while message = get_message
76
+ yield message.message, message.topic
77
+ end
78
+ end
79
+
80
+ def connected?
81
+ @socket && !@socket.closed?
82
+ end
83
+
84
+ private
85
+
86
+ def send_packet packet
87
+ @packet_handler.write_packet packet
88
+ end
89
+
90
+ def receive_packet
91
+ @packet_handler.get_packet
92
+ end
93
+
94
+ def handle_received_packet packet
95
+ case packet
96
+ when PingrespPacket
97
+ @last_ping_response = Time.now
98
+
99
+ when PublishPacket
100
+ @received_messages << packet
101
+ if packet.qos > 0
102
+ send_packet PubackPacket.new packet_id: packet.packet_id
103
+ end
104
+
105
+ when PubackPacket
106
+ end
107
+ end
108
+
109
+ def generate_client_id
110
+ "client_#{ rand(10000000) }"
111
+ end
112
+
113
+ def spawn_read_thread!
114
+ @read_thread = Thread.new do
115
+ while connected? do
116
+ handle_received_packet receive_packet
117
+ end
118
+ @received_messages << nil
119
+ end
120
+ end
121
+
122
+ def spawn_keepalive_thread!
123
+ @keepalive_thread = Thread.new do
124
+ while connected? do
125
+ send_packet PingreqPacket.new
126
+ sleep @keep_alive
127
+ if Time.now - @last_ping_response > 2 * @keep_alive
128
+ puts "Error: MQTT Server not responding to ping. Disconnecting."
129
+ @socket.close
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,43 @@
1
+ module MiniMqtt
2
+ class ConnectPacket < Packet
3
+ def initialize options = {}
4
+ @user = options[:user]
5
+ @password = options[:password]
6
+ @client_id = options[:client_id]
7
+ @will_message = options[:will_message]
8
+ @will_topic = options[:will_topic]
9
+ @will_retain = options[:will_retain]
10
+ @will_qos = options[:will_qos] || 0
11
+ @clean_session = options[:clean_session]
12
+ @keep_alive = options[:keep_alive]
13
+ end
14
+
15
+ def build_variable_header
16
+ # Protocol name
17
+ header = ushort(4) # length of name
18
+ header << 'MQTT' # name
19
+ # Protocol level
20
+ header << uchar(4)
21
+ # Flags
22
+ byte = flag_byte [ @user, @password, @will_retain, nil, nil,
23
+ @will_message, @clean_session, nil]
24
+ byte |= @will_qos << 3
25
+ header << uchar(byte)
26
+ #Keepalive
27
+ header << ushort(@keep_alive)
28
+ header
29
+ end
30
+
31
+ def build_payload
32
+ payload = ""
33
+ payload << mqtt_utf8_encode(@client_id)
34
+ if @will_message
35
+ payload << mqtt_utf8_encode(@will_topic)
36
+ payload << mqtt_utf8_encode(@will_message)
37
+ end
38
+ payload << mqtt_utf8_encode(@user) if @user
39
+ payload << mqtt_utf8_encode(@password) if @password
40
+ payload
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,239 @@
1
+ class InvalidFlagsError < StandardError ; end
2
+
3
+ module MiniMqtt
4
+ class Packet
5
+ include BinHelper
6
+
7
+ @@last_packet_id = 0
8
+
9
+ def decode stream, flags = 0
10
+ @stream = stream
11
+ handle_flags flags
12
+ read_variable_header
13
+ read_payload
14
+ self
15
+ end
16
+
17
+ def encode
18
+ build_variable_header + build_payload
19
+ end
20
+
21
+ def flags
22
+ 0b000
23
+ end
24
+
25
+ private
26
+
27
+ def new_packet_id
28
+ @@last_packet_id += 1
29
+ @@last_packet_id %= 65535
30
+ 1 + @@last_packet_id
31
+ end
32
+
33
+ def read_variable_header
34
+ end
35
+
36
+ def read_payload
37
+ end
38
+
39
+ def handle_flags flags
40
+ end
41
+
42
+ def build_variable_header
43
+ ""
44
+ end
45
+
46
+ def build_payload
47
+ ""
48
+ end
49
+
50
+ end
51
+
52
+ module AckPacket
53
+ attr_accessor :packet_id
54
+
55
+ def initialize params = {}
56
+ @packet_id = params[:packet_id]
57
+ end
58
+
59
+ def read_variable_header
60
+ @packet_id = read_ushort @stream
61
+ end
62
+
63
+ def build_variable_header
64
+ ushort @packet_id
65
+ end
66
+ end
67
+
68
+ class ConnackPacket < Packet
69
+ attr_reader :return_code
70
+
71
+ ERRORS = { 1 => "unacceptable protocol version",
72
+ 2 => "identifier rejected",
73
+ 3 => "server unavailable",
74
+ 4 => "bad username or password",
75
+ 5 => "not authorized" }
76
+
77
+
78
+ def read_variable_header
79
+ @session_present = @stream.readbyte & 0x01
80
+ @return_code = @stream.readbyte
81
+ end
82
+
83
+ def session_present?
84
+ @session_present == 1
85
+ end
86
+
87
+ def accepted?
88
+ @return_code == 0
89
+ end
90
+
91
+ def error_message
92
+ ERRORS[@return_code]
93
+ end
94
+
95
+ end
96
+
97
+ class PublishPacket < Packet
98
+ attr_accessor :dup, :qos, :retain, :packet_id, :topic, :message
99
+
100
+ def handle_flags flags
101
+ @dup = flags & 0b1000 != 0
102
+ @qos = (flags & 0b0110) >> 1
103
+ @retain = flags & 0b0001 != 0
104
+ end
105
+
106
+ def read_variable_header
107
+ @topic = read_mqtt_encoded_string @stream
108
+ if @qos > 0
109
+ @packet_id = read_ushort @stream
110
+ end
111
+ end
112
+
113
+ def read_payload
114
+ @message = @stream.read
115
+ end
116
+
117
+ def initialize params = {}
118
+ @topic = params[:topic] || ""
119
+ @message = params[:message] || ""
120
+ @qos = params[:qos] || 0
121
+ @dup = params[:dup] || false
122
+ @retain = params[:retain] || false
123
+ @packet_id = params[:packet_id]
124
+ end
125
+
126
+ def flags
127
+ flags = 0
128
+ flags |= 0b0001 if @retain
129
+ flags |= qos << 1
130
+ flags |= 0b1000 if @dup
131
+ flags
132
+ end
133
+
134
+ def build_variable_header
135
+ header = mqtt_utf8_encode @topic
136
+ if @qos > 0
137
+ @packet_id ||= new_packet_id
138
+ header << ushort(@packet_id)
139
+ end
140
+ header
141
+ end
142
+
143
+ def build_payload
144
+ @message
145
+ end
146
+
147
+ end
148
+
149
+ class PubackPacket < Packet
150
+ include AckPacket
151
+ end
152
+
153
+ class PubrecPacket < Packet
154
+ include AckPacket
155
+ end
156
+
157
+ class PubrelPacket < Packet
158
+ include AckPacket
159
+
160
+ def flags
161
+ 0b0010
162
+ end
163
+ end
164
+
165
+ class PubcompPacket < Packet
166
+ include AckPacket
167
+ end
168
+
169
+ class SubscribePacket < Packet
170
+ attr_accessor :packet_id, :topics
171
+
172
+ def initialize params = {}
173
+ @topics = params[:topics]
174
+ end
175
+
176
+ def flags
177
+ 0b0010
178
+ end
179
+
180
+ def build_variable_header
181
+ @packet_id ||= new_packet_id
182
+ ushort @packet_id
183
+ end
184
+
185
+ def build_payload
186
+ @topics.map do |topic, qos|
187
+ mqtt_utf8_encode(topic) + uchar(qos)
188
+ end.join
189
+ end
190
+ end
191
+
192
+ class SubackPacket < Packet
193
+ attr_accessor :packet_id, :max_qos_accepted
194
+
195
+ def read_variable_header
196
+ @packet_id = read_ushort @stream
197
+ end
198
+
199
+ def read_payload
200
+ @max_qos_accepted = @stream.read.unpack 'C*'
201
+ end
202
+ end
203
+
204
+ class UnsubscribePacket < Packet
205
+ attr_accessor :packet_id, :topics
206
+
207
+ def flags
208
+ 0b0010
209
+ end
210
+
211
+ def initialize params = {}
212
+ @topics = params[:topics]
213
+ end
214
+
215
+ def build_variable_header
216
+ @packet_id ||= new_packet_id
217
+ ushort @packet_id
218
+ end
219
+
220
+ def build_payload
221
+ @topics.map do |topic|
222
+ mqtt_utf8_encode(topic)
223
+ end.join
224
+ end
225
+ end
226
+
227
+ class UnsubackPacket < Packet
228
+ include AckPacket
229
+ end
230
+
231
+ class PingreqPacket < Packet
232
+ end
233
+
234
+ class PingrespPacket < Packet
235
+ end
236
+
237
+ class DisconnectPacket < Packet
238
+ end
239
+ end
@@ -0,0 +1,111 @@
1
+ require 'stringio'
2
+
3
+ module MiniMqtt
4
+ PACKET_CLASSES = { 1 => ConnectPacket,
5
+ 2 => ConnackPacket,
6
+ 3 => PublishPacket,
7
+ 4 => PubackPacket,
8
+ 5 => PubrecPacket,
9
+ 6 => PubrelPacket,
10
+ 7 => PubcompPacket,
11
+ 8 => SubscribePacket,
12
+ 9 => SubackPacket,
13
+ 10 => UnsubscribePacket,
14
+ 11 => UnsubackPacket,
15
+ 12 => PingreqPacket,
16
+ 13 => PingrespPacket,
17
+ 14 => DisconnectPacket }
18
+ PACKET_CODES = PACKET_CLASSES.invert
19
+
20
+ class PacketHandler
21
+ include BinHelper
22
+
23
+ MAX_LENGTH_MULTIPLIER = 128 ** 3
24
+
25
+ @@debug = false
26
+
27
+ def self.enable_debug
28
+ @@debug = true
29
+ end
30
+
31
+ def initialize stream
32
+ @stream = stream
33
+ @mutex = Mutex.new
34
+ end
35
+
36
+ def get_packet
37
+ # First byte contains packet type and flags. 4 bits each.
38
+ first_byte = @stream.readbyte
39
+ packet_class = PACKET_CLASSES[ first_byte >> 4 ]
40
+ flags = first_byte & 0xf
41
+
42
+ #Decode length using algorithm, and read packet body.
43
+ length = decode_length @stream
44
+ encoded_packet = @stream.read length
45
+ log_in_packet packet_class, encoded_packet
46
+
47
+ # Create appropiate packet instance and decode the packet body.
48
+ packet_class.new.decode StringIO.new(encoded_packet), flags
49
+
50
+ rescue StandardError => e
51
+ log "Exception while receiving: #{ e.inspect }"
52
+ @stream.close
53
+ end
54
+
55
+ def write_packet packet
56
+ # Write type and flags, then encoded packet length, then packet
57
+ @mutex.synchronize do
58
+ type_and_flags = PACKET_CODES[packet.class] << 4
59
+ type_and_flags += packet.flags
60
+ @stream.write uchar(type_and_flags)
61
+ encoded_packet = packet.encode
62
+ log_out_packet packet
63
+ @stream.write encode_length(encoded_packet.length)
64
+ @stream.write encoded_packet
65
+ end
66
+ rescue StandardError => e
67
+ log "Exception while receiving: #{ e.inspect }"
68
+ @stream.close
69
+ end
70
+
71
+ private
72
+
73
+ def encode_length length
74
+ encoded = ""
75
+ loop do
76
+ encoded_byte = length % 128
77
+ length = length / 128
78
+ encoded_byte |= 128 if length > 0
79
+ encoded << encoded_byte.chr
80
+ break if length == 0
81
+ end
82
+ encoded
83
+ end
84
+
85
+ def decode_length stream
86
+ length = 0
87
+ multiplier = 1
88
+ while encoded_byte = stream.readbyte
89
+ length += (encoded_byte & 0x7f) * multiplier
90
+ break if encoded_byte & 0x80 == 0
91
+ multiplier *= 128
92
+ raise "Malformed remaining length" if multiplier > MAX_LENGTH_MULTIPLIER
93
+ end
94
+ length
95
+ end
96
+
97
+ def log_in_packet type, message
98
+ log "\nIN - #{ type } - #{ message.inspect }\n"
99
+ end
100
+
101
+ def log_out_packet packet
102
+ log "\nOUT - #{ packet.class } - #{ packet.instance_variable_get :@packet_id } - #{ packet.encode.inspect }\n"
103
+ end
104
+
105
+ def log text
106
+ if @@debug
107
+ puts text
108
+ end
109
+ end
110
+ end
111
+ end
data/lib/mini_mqtt.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'mini_mqtt/bin_helper'
2
+ require 'mini_mqtt/packet'
3
+ require 'mini_mqtt/connect_packet'
4
+ require 'mini_mqtt/packet_handler'
5
+ require 'mini_mqtt/client'
6
+
7
+ module MiniMqtt
8
+ end
@@ -0,0 +1,119 @@
1
+ require 'test_helper'
2
+ require 'socket'
3
+ require 'timeout'
4
+
5
+ class ClientTest < MiniTest::Test
6
+ TEST_SERVER = 'test.mosquitto.org'
7
+
8
+ def setup
9
+ @client = Client.new host: TEST_SERVER
10
+ end
11
+
12
+ def test_mosquitto_server_is_running
13
+ begin
14
+ socket = TCPSocket.new TEST_SERVER, 1883
15
+ socket.close
16
+ rescue
17
+ puts "There should be a mosquitto server running to run tests."
18
+ end
19
+ end
20
+
21
+ def test_connect_and_disconnect
22
+ @client.connect
23
+ assert @client.connected?
24
+ @client.disconnect
25
+ refute @client.connected?
26
+ end
27
+
28
+ def test_subscribe_and_publish
29
+ @client.connect
30
+ @client.subscribe '/mini_mqtt/test'
31
+ @client.publish '/mini_mqtt/test', 'hi'
32
+ msg = @client.get_message
33
+ assert_equal '/mini_mqtt/test', msg.topic
34
+ assert_equal 'hi', msg.message
35
+ @client.disconnect
36
+ end
37
+
38
+ def test_subscribe_multiple_topics
39
+ @client.connect
40
+ @client.subscribe '/mini_mqtt/test1', '/mini_mqtt/test2'
41
+ @client.publish '/mini_mqtt/test1', 'message_1'
42
+ @client.publish '/mini_mqtt/test2', 'message_2'
43
+ expected = ['message_1', 'message_2']
44
+ @client.get_messages do |message|
45
+ assert_equal expected.shift, message
46
+ break if expected.empty?
47
+ end
48
+ @client.disconnect
49
+ end
50
+
51
+ def test_unsubscribe
52
+ @client.connect
53
+ @client.subscribe '/mini_mqtt/test'
54
+ @client.unsubscribe '/mini_mqtt/test'
55
+ @client.publish '/mini_mqtt/test', 'hi'
56
+ assert_raises(Timeout::Error) do
57
+ Timeout::timeout(1) do
58
+ @client.get_message
59
+ end
60
+ end
61
+ @client.disconnect
62
+ end
63
+
64
+ def test_retain_message
65
+ @client.connect
66
+ message_to_retain = rand.to_s
67
+ @client.publish '/mini_mqtt/retain', message_to_retain, retain: true
68
+ @client.subscribe '/mini_mqtt/retain'
69
+ assert_equal message_to_retain, @client.get_message.message
70
+ @client.disconnect
71
+ end
72
+
73
+ def test_clean_session
74
+ @client = MiniMqtt::Client.new host: TEST_SERVER, clean_session: false
75
+ @client.connect
76
+ @client.subscribe '/mini_mqtt/test'
77
+ @client.disconnect
78
+ @client.connect
79
+ @client.publish '/mini_mqtt/test', 'hello'
80
+ assert_equal 'hello', @client.get_message.message
81
+ @client.disconnect
82
+ end
83
+
84
+ def test_last_will
85
+ @client2 = MiniMqtt::Client.new host: TEST_SERVER
86
+ @client2.connect
87
+ @client2.subscribe 'mini_mqtt/last_will'
88
+ sleep 3 # wait for suback to come back
89
+ @client.connect will_topic: 'mini_mqtt/last_will', will_message: 'help!!'
90
+ # abruptly close connection by closing socket.
91
+ @client.instance_variable_get(:@socket).close
92
+ assert_equal 'help!!', @client2.get_message.message
93
+ @client2.disconnect
94
+ end
95
+
96
+ def test_last_will_with_retain
97
+ will_msg = rand.to_s
98
+ @client.connect will_topic: 'mini_mqtt/last_will_retain', will_message: will_msg,
99
+ will_retain: true
100
+ assert @client.connected?
101
+ @client.instance_variable_get(:@socket).close
102
+ @client.connect
103
+ @client.subscribe 'mini_mqtt/last_will_retain'
104
+ assert_equal will_msg, @client.get_message.message
105
+ @client.disconnect
106
+ end
107
+
108
+ def test_qos_1
109
+ @client.connect
110
+ @client.subscribe '/mini_mqtt/topic_qos' => 1
111
+ @client.publish '/mini_mqtt/topic_qos', 'msg', qos: 1
112
+ msg = @client.get_message
113
+ assert_equal '/mini_mqtt/topic_qos', msg.topic
114
+ assert_equal 'msg', msg.message
115
+ assert_equal 1, msg.qos
116
+ @client.disconnect
117
+ end
118
+ end
119
+
@@ -0,0 +1,18 @@
1
+ require 'test_helper'
2
+
3
+ class ConnackPacketTest < MiniTest::Test
4
+ def test_decode_accepted_connection
5
+ packet = ConnackPacket.new
6
+ packet.decode "\x01\x00".to_stream
7
+ assert packet.session_present?
8
+ assert packet.accepted?
9
+ end
10
+
11
+ def test_decode_refused_connection
12
+ packet = ConnackPacket.new
13
+ packet.decode "\x00\x04".to_stream
14
+ assert_equal false, packet.session_present?
15
+ assert_equal false, packet.accepted?
16
+ assert_match(/bad username or password/, packet.error_message)
17
+ end
18
+ end
@@ -0,0 +1,25 @@
1
+ require 'test_helper'
2
+
3
+ class ConnectPacketTest < MiniTest::Test
4
+ def test_encode_with_all_params
5
+ packet = ConnectPacket.new user: 'arman',
6
+ password: 'secret',
7
+ client_id: 'abc',
8
+ will_message: 'I am dead',
9
+ will_topic: 'last_will',
10
+ will_retain: false,
11
+ will_qos: 0,
12
+ clean_session: true,
13
+ keep_alive: 20
14
+ encoded = packet.encode
15
+ assert_equal "\x00\x04MQTT", encoded[0..5]
16
+ assert_equal "\x04", encoded[6]
17
+ assert_equal [0xc6], encoded[7].bytes
18
+ assert_equal "\x00\x14", encoded[8..9]
19
+ assert_equal "\x00\x03abc", encoded[10..14]
20
+ assert_equal "\x00\x09last_will", encoded[15..25]
21
+ assert_equal "\x00\x09I am dead", encoded[26..36]
22
+ assert_equal "\x00\x05arman", encoded[37..43]
23
+ assert_equal "\x00\x06secret", encoded[44..-1]
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ require 'test_helper'
2
+
3
+ class DisconnectPacketTest < MiniTest::Test
4
+ def test_encode
5
+ packet = DisconnectPacket.new
6
+ assert_equal "", packet.encode
7
+ end
8
+
9
+ def test_flags
10
+ packet = DisconnectPacket.new
11
+ assert_equal 0, packet.flags
12
+ end
13
+ end
@@ -0,0 +1,53 @@
1
+ require 'test_helper'
2
+
3
+ class TestPacketHandler < Minitest::Test
4
+ def setup
5
+ @socket = StringIO.new
6
+ @handler = PacketHandler.new @socket
7
+ end
8
+
9
+ def test_get_packet
10
+ @socket.write "\xd0\x00"
11
+ @socket.write "\xa2\x05topic"
12
+ @socket.rewind
13
+ packet = @handler.get_packet
14
+ assert_equal PingrespPacket, packet.class
15
+
16
+ packet = @handler.get_packet
17
+ assert_equal UnsubscribePacket, packet.class
18
+ end
19
+
20
+ def test_write_pingreq_packet
21
+ packet = PingreqPacket.new
22
+ @handler.write_packet packet
23
+ @socket.rewind
24
+ assert_equal 0xc0, @socket.readbyte # packet type and flags
25
+ assert_equal 0x00, @socket.readbyte # encoded length
26
+ end
27
+
28
+ def test_write_connect_packet
29
+ packet = ConnectPacket.new client_id: 'abc', keep_alive: 15
30
+ @handler.write_packet packet
31
+ @socket.rewind
32
+ assert_equal 0x10, @socket.readbyte # packet type and flags
33
+ assert_equal 0x0F, @socket.readbyte # encoded length
34
+ assert_equal @socket.read, packet.encode
35
+ end
36
+
37
+ def test_length_encoding
38
+ assert_equal [0x00], @handler.send(:encode_length, 0).bytes
39
+ assert_equal [0xc1,0x02], @handler.send(:encode_length, 321).bytes
40
+ assert_equal [0x80,0x80,0x01], @handler.send(:encode_length, 16384).bytes
41
+ assert_equal [0xff,0xff,0xff,0x7f], @handler.send(:encode_length, 268_435_455).bytes
42
+ end
43
+
44
+ def test_length_decoding
45
+ assert_equal 0, @handler.send(:decode_length, "\x00".to_stream)
46
+ assert_equal 321, @handler.send(:decode_length, "\xC1\x02".to_stream)
47
+ assert_equal 16384, @handler.send(:decode_length, "\x80\x80\x01".to_stream)
48
+ assert_equal 268_435_455, @handler.send(:decode_length, "\xFF\xFF\xFF\x7F".to_stream)
49
+ assert_raises StandardError do
50
+ @handler.send(:decode_length, "\xFF\xFF\xFF\xFF".to_stream)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,13 @@
1
+ require 'test_helper'
2
+
3
+ class PingPacketTest < MiniTest::Test
4
+ def test_pingreq_packet
5
+ packet = PingreqPacket.new
6
+ assert_equal "", packet.encode
7
+ end
8
+
9
+ def test_pingresp_packet
10
+ packet = PingrespPacket.new.decode ""
11
+ assert packet
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require 'test_helper'
2
+
3
+ class PubackPacketTest < MiniTest::Test
4
+ def test_decode
5
+ puback = PubackPacket.new.decode "\x00\xFF".to_stream
6
+ assert_equal puback.packet_id, 0xFF
7
+ end
8
+
9
+ def test_encode
10
+ puback = PubackPacket.new packet_id: 9
11
+ assert_equal puback.encode, "\x00\x09"
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require 'test_helper'
2
+
3
+ class PubcompPacketTest < MiniTest::Test
4
+ def test_decode
5
+ pubcomp = PubcompPacket.new.decode "\x00\xFF".to_stream
6
+ assert_equal pubcomp.packet_id, 0xFF
7
+ end
8
+
9
+ def test_encode
10
+ pubcomp = PubcompPacket.new packet_id: 9
11
+ assert_equal pubcomp.encode, "\x00\x09"
12
+ end
13
+ end
@@ -0,0 +1,44 @@
1
+ require 'test_helper'
2
+
3
+ class PublishPacketTest < MiniTest::Test
4
+ def setup
5
+ @inbound_publish_1 = PublishPacket.new.decode "\x00\x03a/b\x00\x0aMessageHere".to_stream, 0b1011
6
+ @inbound_publish_2 = PublishPacket.new.decode "\x00\x03a/bMessageHere".to_stream, 0b0000
7
+ @outbound_publish_1 = PublishPacket.new topic: 'help', message: 'SOS'
8
+ @outbound_publish_2 = PublishPacket.new topic: 'help', message: 'SOS',
9
+ qos: 1, dup: true, retain: true, packet_id: 5
10
+ end
11
+
12
+ def test_read_flags
13
+ assert @inbound_publish_1.dup
14
+ assert @inbound_publish_1.retain
15
+ assert_equal 1, @inbound_publish_1.qos
16
+
17
+ refute @inbound_publish_2.dup
18
+ refute @inbound_publish_2.retain
19
+ assert_equal 0, @inbound_publish_2.qos
20
+ end
21
+
22
+ def test_decode_topic_and_packet_id
23
+ assert_equal 'a/b', @inbound_publish_1.topic
24
+ assert_equal 10, @inbound_publish_1.packet_id
25
+
26
+ assert_equal 'a/b', @inbound_publish_2.topic
27
+ assert_equal nil, @inbound_publish_2.packet_id
28
+ end
29
+
30
+ def test_decode_message
31
+ assert_equal 'MessageHere', @inbound_publish_1.message
32
+ assert_equal 'MessageHere', @inbound_publish_2.message
33
+ end
34
+
35
+ def test_encode_flags
36
+ assert_equal 0b0000, @outbound_publish_1.flags
37
+ assert_equal 0b1011, @outbound_publish_2.flags
38
+ end
39
+
40
+ def test_encode_message
41
+ assert_equal "\x00\x04helpSOS", @outbound_publish_1.encode
42
+ assert_equal "\x00\x04help\x00\x05SOS", @outbound_publish_2.encode
43
+ end
44
+ end
@@ -0,0 +1,13 @@
1
+ require 'test_helper'
2
+
3
+ class PubrecPacketTest < MiniTest::Test
4
+ def test_decode
5
+ pubrec = PubrecPacket.new.decode "\x00\xFF".to_stream
6
+ assert_equal pubrec.packet_id, 0xFF
7
+ end
8
+
9
+ def test_encode
10
+ pubrec = PubrecPacket.new packet_id: 9
11
+ assert_equal pubrec.encode, "\x00\x09"
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ require 'test_helper'
2
+
3
+ class PubrelPacketTest < MiniTest::Test
4
+ def test_decode
5
+ pubrel = PubrelPacket.new.decode "\x00\xFF".to_stream
6
+ assert_equal pubrel.packet_id, 0xFF
7
+ end
8
+
9
+ def test_encode_and_flags
10
+ pubrel = PubrelPacket.new packet_id: 9
11
+ assert_equal pubrel.encode, "\x00\x09"
12
+ assert_equal 0b0010, pubrel.flags
13
+ end
14
+
15
+ end
@@ -0,0 +1,10 @@
1
+ require 'test_helper'
2
+
3
+ class SubackPacketTest < MiniTest::Test
4
+ def test_decode
5
+ packet = SubackPacket.new
6
+ packet.decode "\x01\x00\x01\x00\x02".to_stream
7
+ assert_equal 256, packet.packet_id
8
+ assert_equal [1, 0, 2], packet.max_qos_accepted
9
+ end
10
+ end
@@ -0,0 +1,27 @@
1
+ require 'test_helper'
2
+
3
+ class SubscribePacketTest < MiniTest::Test
4
+ def setup
5
+ @packet = SubscribePacket.new topics: { 'topic1' => 0, 'topic2' => 1 }
6
+ @packet2 = SubscribePacket.new topics: { 'topic3' => 0 }
7
+ end
8
+
9
+ def test_flags
10
+ assert_equal 0b0010, @packet.flags
11
+ end
12
+
13
+ def test_packet_ids
14
+ @packet.encode
15
+ @packet2.encode
16
+ assert @packet.packet_id > 0
17
+ assert @packet2.packet_id > @packet.packet_id
18
+ end
19
+
20
+ def test_encode
21
+ @packet.packet_id = 1
22
+ @packet2.packet_id = 2
23
+ assert_equal "\x00\x01\x00\x06topic1\x00\x00\x06topic2\x01", @packet.encode
24
+ assert_equal "\x00\x02\x00\x06topic3\x00", @packet2.encode
25
+ end
26
+
27
+ end
@@ -0,0 +1,17 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
3
+
4
+ $:.unshift File.join( __FILE__, "..", "..", "lib")
5
+ require 'pry'
6
+ require 'mini_mqtt'
7
+ require 'minitest/autorun'
8
+
9
+ class String
10
+ def to_stream
11
+ StringIO.new self
12
+ end
13
+ end
14
+
15
+ class MiniTest::Test
16
+ include MiniMqtt
17
+ end
@@ -0,0 +1,8 @@
1
+ require 'test_helper'
2
+
3
+ class UnsubackPacketTest < MiniTest::Test
4
+ def test_decode
5
+ packet = UnsubackPacket.new.decode "\x00\xFF".to_stream
6
+ assert_equal packet.packet_id, 0xFF
7
+ end
8
+ end
@@ -0,0 +1,22 @@
1
+ require 'test_helper'
2
+
3
+ class UnsubscribePacketTest < MiniTest::Test
4
+ def setup
5
+ @packet = UnsubscribePacket.new topics: ['topic1', 'topic2']
6
+ end
7
+
8
+ def test_flags
9
+ assert_equal 0b0010, @packet.flags
10
+ end
11
+
12
+ def test_packet_id
13
+ @packet.encode
14
+ assert @packet.packet_id > 0
15
+ end
16
+
17
+ def test_encode
18
+ @packet.packet_id = 1
19
+ assert_equal "\x00\x01\x00\x06topic1\x00\x06topic2", @packet.encode
20
+ end
21
+
22
+ end
metadata ADDED
@@ -0,0 +1,152 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mini_mqtt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Armando Andini
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-10-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.0
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 1.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.0.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 1.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 1.0.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 1.0.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 0.1.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 0.1.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 0.1.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 0.1.0
83
+ description: Minimalist implementation of MQTT client purely in Ruby.
84
+ email: armando.andini@hotmail.com
85
+ executables: []
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - README.md
90
+ - lib/mini_mqtt.rb
91
+ - lib/mini_mqtt/bin_helper.rb
92
+ - lib/mini_mqtt/client.rb
93
+ - lib/mini_mqtt/connect_packet.rb
94
+ - lib/mini_mqtt/packet.rb
95
+ - lib/mini_mqtt/packet_handler.rb
96
+ - test/client_test.rb
97
+ - test/connack_packet_test.rb
98
+ - test/connect_packet_test.rb
99
+ - test/disconnect_packet_test.rb
100
+ - test/packet_handler_test.rb
101
+ - test/ping_packets_test.rb
102
+ - test/puback_packet_test.rb
103
+ - test/pubcomp_packet_test.rb
104
+ - test/publish_packet_test.rb
105
+ - test/pubrec_packet_test.rb
106
+ - test/pubrel_packet_test.rb
107
+ - test/suback_packet_test.rb
108
+ - test/subscribe_packet_test.rb
109
+ - test/test_helper.rb
110
+ - test/unsuback_packet_test.rb
111
+ - test/unsubscribe_packet_test.rb
112
+ homepage: http://github.com/antico5/mini_mqtt
113
+ licenses:
114
+ - MIT
115
+ metadata: {}
116
+ post_install_message:
117
+ rdoc_options: []
118
+ require_paths:
119
+ - lib
120
+ required_ruby_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ requirements: []
131
+ rubyforge_project:
132
+ rubygems_version: 2.5.1
133
+ signing_key:
134
+ specification_version: 4
135
+ summary: MQTT Client for Ruby.
136
+ test_files:
137
+ - test/pubcomp_packet_test.rb
138
+ - test/disconnect_packet_test.rb
139
+ - test/publish_packet_test.rb
140
+ - test/ping_packets_test.rb
141
+ - test/connack_packet_test.rb
142
+ - test/unsubscribe_packet_test.rb
143
+ - test/connect_packet_test.rb
144
+ - test/test_helper.rb
145
+ - test/subscribe_packet_test.rb
146
+ - test/unsuback_packet_test.rb
147
+ - test/suback_packet_test.rb
148
+ - test/puback_packet_test.rb
149
+ - test/client_test.rb
150
+ - test/pubrel_packet_test.rb
151
+ - test/pubrec_packet_test.rb
152
+ - test/packet_handler_test.rb