ruby-mqtt3 1.0.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 +7 -0
- data/lib/ruby-mqtt3.rb +663 -0
- metadata +43 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 68bf5d092ad9497fd2bce86c75325cb00438f89b6bc5cb92f6c0ae2221f9e379
|
4
|
+
data.tar.gz: f7fe2882e4e2fe44013ed4e235dbdebdb892fcd28351d112708e00214abef7d5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d71f5787b3b639396050432b001220f4f46571aea8af362673c2c6f3fdf1d82e382b40a1534c08907610f18f3a7a652885c44a5863d5edfac5c28b81e9e4f344
|
7
|
+
data.tar.gz: fe9545a26a3135d7e8ab0ceb91c62bf0ae2d7189600b0fa5c04a58dd0405a86171b63301bff9d670b85ecc9954b2da133704091f22615a6d502f0ebfb2743a5d
|
data/lib/ruby-mqtt3.rb
ADDED
@@ -0,0 +1,663 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
class Mqtt3NormalExitException < Exception
|
4
|
+
end
|
5
|
+
|
6
|
+
class Mqtt3
|
7
|
+
attr_accessor :debug
|
8
|
+
|
9
|
+
#connection params
|
10
|
+
attr_accessor :host
|
11
|
+
attr_accessor :ip
|
12
|
+
attr_accessor :reconnect
|
13
|
+
attr_accessor :keepalive_sec
|
14
|
+
attr_accessor :client_id
|
15
|
+
attr_accessor :clean_session
|
16
|
+
attr_accessor :will_topic
|
17
|
+
attr_accessor :will_payload
|
18
|
+
attr_accessor :will_qos
|
19
|
+
attr_accessor :will_retain
|
20
|
+
attr_accessor :username
|
21
|
+
attr_accessor :password
|
22
|
+
attr_accessor :persistence_filename
|
23
|
+
attr_accessor :persistence_mode
|
24
|
+
|
25
|
+
attr_accessor :ssl
|
26
|
+
attr_accessor :ssl_cert
|
27
|
+
attr_accessor :ssl_cert_file
|
28
|
+
attr_accessor :ssl_key
|
29
|
+
attr_accessor :ssl_key_file
|
30
|
+
attr_accessor :ssl_ca_file
|
31
|
+
attr_accessor :ssl_passphrase
|
32
|
+
|
33
|
+
#internal state
|
34
|
+
attr_reader :last_packet_sent_at
|
35
|
+
attr_reader :packet_id
|
36
|
+
attr_reader :state
|
37
|
+
attr_reader :ssl_context
|
38
|
+
|
39
|
+
MQTT_PACKET_TYPES = [
|
40
|
+
'INVALID', #0
|
41
|
+
'CONNECT', #1
|
42
|
+
'CONNACK', #2
|
43
|
+
'PUBLISH', #3
|
44
|
+
'PUBACK', #4
|
45
|
+
'PUBREC', #5
|
46
|
+
'PUBREL', #6
|
47
|
+
'PUBCOMP', #7
|
48
|
+
'SUBSCRIBE', #8
|
49
|
+
'SUBACK', #9
|
50
|
+
'UNSUBSCRIBE', #10
|
51
|
+
'UNSUBACK', #11
|
52
|
+
'PINGREQ', #12
|
53
|
+
'PINGRESP', #13
|
54
|
+
'DISCONNECT', #14
|
55
|
+
'RESERVED' ].freeze
|
56
|
+
|
57
|
+
CONNECT = 1
|
58
|
+
CONNACK = 2
|
59
|
+
PUBLISH = 3
|
60
|
+
PUBACK = 4
|
61
|
+
PUBREC = 5
|
62
|
+
PUBREL = 6
|
63
|
+
PUBCOMP = 7
|
64
|
+
SUBSCRIBE = 8
|
65
|
+
SUBACK = 9
|
66
|
+
UNSUBSCRIBE = 10
|
67
|
+
UNSUBACK = 11
|
68
|
+
PINGREQ = 12
|
69
|
+
PINGRESP = 13
|
70
|
+
DISCONNECT = 14
|
71
|
+
|
72
|
+
def initialize(host: 'localhost',
|
73
|
+
port: 1883,
|
74
|
+
reconnect: true,
|
75
|
+
keepalive_sec: 30,
|
76
|
+
client_id: nil,
|
77
|
+
clean_session: true,
|
78
|
+
will_topic: nil,
|
79
|
+
will_payload: nil,
|
80
|
+
will_qos: 0,
|
81
|
+
will_retain: false,
|
82
|
+
username: nil,
|
83
|
+
password: nil,
|
84
|
+
persistence_filename: nil,
|
85
|
+
persistence_mode: :save_manual, # or :save_everytime
|
86
|
+
ssl: nil,
|
87
|
+
ssl_cert: nil,
|
88
|
+
ssl_cert_file: nil,
|
89
|
+
ssl_key: nil,
|
90
|
+
ssl_key_file: nil,
|
91
|
+
ssl_ca_file: nil,
|
92
|
+
ssl_passphrase: nil)
|
93
|
+
@host = host
|
94
|
+
@port = port
|
95
|
+
@reconnect = reconnect
|
96
|
+
@keepalive_sec = keepalive_sec
|
97
|
+
@client_id = client_id
|
98
|
+
if @client_id.nil?
|
99
|
+
@client_id = File.basename($0)[0..10]
|
100
|
+
charset = Array('A'..'Z') + Array('a'..'z') + Array('0'..'9')
|
101
|
+
@client_id += '-' + Array.new(8) { charset.sample }.join
|
102
|
+
end
|
103
|
+
@clean_session = clean_session
|
104
|
+
@will_topic = will_topic
|
105
|
+
@will_payload = will_payload
|
106
|
+
@will_qos = will_qos
|
107
|
+
@will_retain = will_retain
|
108
|
+
@username = username
|
109
|
+
@password = password
|
110
|
+
@persistence_filename = persistence_filename
|
111
|
+
@persistence_mode = persistence_mode
|
112
|
+
|
113
|
+
@ssl = ssl
|
114
|
+
@ssl_cert = ssl_cert
|
115
|
+
@ssl_cert_file = ssl_cert_file
|
116
|
+
@ssl_key = ssl_key
|
117
|
+
@ssl_key_file = ssl_key_file
|
118
|
+
@ssl_ca_file = ssl_ca_file
|
119
|
+
@ssl_passphrase = ssl_passphrase
|
120
|
+
|
121
|
+
init_ssl() if @ssl
|
122
|
+
|
123
|
+
@socket = nil
|
124
|
+
@packet_id = 0
|
125
|
+
@outgoing_qos1_store = Hash.new
|
126
|
+
@outgoing_qos2_store = Hash.new
|
127
|
+
@incoming_qos1_store = Hash.new
|
128
|
+
@incoming_qos2_store = Hash.new
|
129
|
+
@packet_id = 0
|
130
|
+
@state = :disconnected
|
131
|
+
end
|
132
|
+
|
133
|
+
def pingreq
|
134
|
+
send_packet("\xc0\x00".force_encoding('ASCII-8BIT')) #PINGREQ
|
135
|
+
end
|
136
|
+
|
137
|
+
def pingresp
|
138
|
+
send_packet("\xd0\x00".force_encoding('ASCII-8BIT')) #PINGRESP
|
139
|
+
end
|
140
|
+
|
141
|
+
def disconnect
|
142
|
+
send_packet("\xe0\x00".force_encoding('ASCII-8BIT')) #DISCONNECT
|
143
|
+
end
|
144
|
+
|
145
|
+
def invalid
|
146
|
+
send_packet("\xff\x00".force_encoding('ASCII-8BIT'))
|
147
|
+
end
|
148
|
+
|
149
|
+
def connect
|
150
|
+
body = encode_string('MQTT')
|
151
|
+
body += encode_bytes 0x04
|
152
|
+
|
153
|
+
flags = 0
|
154
|
+
flags |= 0x02 if @clean_session
|
155
|
+
flags |= 0x04 unless @will_topic.nil?
|
156
|
+
flags |= ((@will_qos & 0x03) << 3)
|
157
|
+
flags |= 0x20 if @will_retain
|
158
|
+
flags |= 0x40 unless @password.nil?
|
159
|
+
flags |= 0x80 unless @username.nil?
|
160
|
+
body += encode_bytes(flags)
|
161
|
+
|
162
|
+
body += encode_short(@keepalive_sec)
|
163
|
+
body += encode_string(@client_id)
|
164
|
+
unless will_topic.nil?
|
165
|
+
body += encode_string(@will_topic)
|
166
|
+
body += encode_string(@will_payload)
|
167
|
+
end
|
168
|
+
body += encode_string(@username) unless @username.nil?
|
169
|
+
body += encode_string(@password) unless @password.nil?
|
170
|
+
|
171
|
+
packet = encode_bytes(CONNECT << 4)
|
172
|
+
packet += encode_length(body.length)
|
173
|
+
packet += body
|
174
|
+
|
175
|
+
send_packet(packet)
|
176
|
+
end
|
177
|
+
|
178
|
+
def subscribe(topic_list)
|
179
|
+
body = encode_short(next_packet_id())
|
180
|
+
topic_list.each do |x|
|
181
|
+
body += encode_string(x[0])
|
182
|
+
body += encode_bytes(x[1])
|
183
|
+
end
|
184
|
+
|
185
|
+
flags = 2
|
186
|
+
packet = encode_bytes((SUBSCRIBE << 4) + flags)
|
187
|
+
packet += encode_length(body.length)
|
188
|
+
packet += body
|
189
|
+
|
190
|
+
send_packet(packet)
|
191
|
+
end
|
192
|
+
|
193
|
+
def publish(topic,message,qos = 0,retain = false)
|
194
|
+
publish_dup(topic,message,qos,retain,false,nil)
|
195
|
+
end
|
196
|
+
|
197
|
+
def publish_dup(topic,message,qos = 0,retain = false, dup = false, packet_id = nil)
|
198
|
+
raise 'Invalid topic name' if topic.nil? || topic.to_s.empty?
|
199
|
+
raise 'Invalid QoS' if qos < 0 || qos > 2
|
200
|
+
|
201
|
+
# first publish
|
202
|
+
if packet_id.nil?
|
203
|
+
packet_id = next_packet_id()
|
204
|
+
|
205
|
+
if qos == 1
|
206
|
+
@outgoing_qos1_store[packet_id] = [topic,message,qos,retain]
|
207
|
+
elsif qos == 2
|
208
|
+
@outgoing_qos2_store[packet_id] = [topic,message,qos,retain,PUBLISH]
|
209
|
+
end
|
210
|
+
save_everytime()
|
211
|
+
end
|
212
|
+
|
213
|
+
|
214
|
+
flags = 0
|
215
|
+
flags += 1 if retain
|
216
|
+
flags += qos << 1
|
217
|
+
flags += 8 if dup
|
218
|
+
|
219
|
+
body = encode_string(topic)
|
220
|
+
body += encode_short(packet_id) if qos > 0
|
221
|
+
body += message
|
222
|
+
|
223
|
+
packet = encode_bytes((PUBLISH << 4) + flags)
|
224
|
+
packet += encode_length(body.length)
|
225
|
+
packet += body
|
226
|
+
|
227
|
+
send_packet(packet)
|
228
|
+
return packet_id
|
229
|
+
end
|
230
|
+
|
231
|
+
def puback(packet_id)
|
232
|
+
packet = "\x42\x02".force_encoding('ASCII-8BIT') #PUBACK
|
233
|
+
packet += encode_short(packet_id)
|
234
|
+
send_packet(packet)
|
235
|
+
end
|
236
|
+
|
237
|
+
def pubrec(packet_id)
|
238
|
+
packet = "\x52\x02".force_encoding('ASCII-8BIT') #PUBREC
|
239
|
+
packet += encode_short(packet_id)
|
240
|
+
send_packet(packet)
|
241
|
+
end
|
242
|
+
|
243
|
+
def pubrel(packet_id)
|
244
|
+
packet = "\x62\x02".force_encoding('ASCII-8BIT') #PUBREL
|
245
|
+
packet += encode_short(packet_id)
|
246
|
+
send_packet(packet)
|
247
|
+
end
|
248
|
+
|
249
|
+
def pubcomp(packet_id)
|
250
|
+
packet = "\x72\x02".force_encoding('ASCII-8BIT') #PUBCOMP
|
251
|
+
packet += encode_short(packet_id)
|
252
|
+
send_packet(packet)
|
253
|
+
end
|
254
|
+
|
255
|
+
def next_packet_id
|
256
|
+
@packet_id += 1
|
257
|
+
@packet_id = 0 if @packet_id > 0xffff
|
258
|
+
return @packet_id
|
259
|
+
end
|
260
|
+
|
261
|
+
def encode_bytes(*bytes)
|
262
|
+
bytes.pack('C*')
|
263
|
+
end
|
264
|
+
|
265
|
+
def encode_bits(bits)
|
266
|
+
[bits.map { |b| b ? '1' : '0' }.join].pack('b*')
|
267
|
+
end
|
268
|
+
|
269
|
+
def encode_short(val)
|
270
|
+
raise 'Value too big for short' if val > 0xffff
|
271
|
+
[val.to_i].pack('n')
|
272
|
+
end
|
273
|
+
|
274
|
+
def encode_string(str)
|
275
|
+
str = str.to_s.encode('UTF-8')
|
276
|
+
|
277
|
+
# Force to binary, when assembling the packet
|
278
|
+
str.force_encoding('ASCII-8BIT')
|
279
|
+
encode_short(str.bytesize) + str
|
280
|
+
end
|
281
|
+
|
282
|
+
def encode_length(body_length)
|
283
|
+
if body_length > 268_435_455
|
284
|
+
raise 'Error serialising packet: body is more than 256MB'
|
285
|
+
end
|
286
|
+
|
287
|
+
x = ''
|
288
|
+
loop do
|
289
|
+
digit = (body_length % 128)
|
290
|
+
body_length = body_length.div(128)
|
291
|
+
# if there are more digits to encode, set the top bit of this digit
|
292
|
+
digit |= 0x80 if body_length > 0
|
293
|
+
x += digit.chr
|
294
|
+
break if body_length <= 0
|
295
|
+
end
|
296
|
+
return x
|
297
|
+
end
|
298
|
+
|
299
|
+
def decode_short(bytes)
|
300
|
+
bytes.unpack('n').first
|
301
|
+
end
|
302
|
+
|
303
|
+
def send_packet(p)
|
304
|
+
return false if state == :disconnected
|
305
|
+
return false if state == :tcp_connected && ((p[0].ord >> 4) != CONNECT)
|
306
|
+
|
307
|
+
debug '--- ' + MQTT_PACKET_TYPES[p[0].ord >> 4] + ' flags: ' + (p[0].ord & 0x0f).to_s + ' ' + p.unpack('H*').first
|
308
|
+
|
309
|
+
@socket.write(p)
|
310
|
+
@last_packet_sent_at = Time.now
|
311
|
+
return true
|
312
|
+
end
|
313
|
+
|
314
|
+
def on_connect(&block)
|
315
|
+
@on_connect_block = block
|
316
|
+
end
|
317
|
+
|
318
|
+
def on_tcp_connect_error(&block)
|
319
|
+
@on_tcp_connect_error_block = block
|
320
|
+
end
|
321
|
+
|
322
|
+
def on_mqtt_connect_error(&block)
|
323
|
+
@on_mqtt_connect_error_block = block
|
324
|
+
end
|
325
|
+
|
326
|
+
def on_disconnect(&block)
|
327
|
+
@on_disconnect_block = block
|
328
|
+
end
|
329
|
+
|
330
|
+
def on_subscribe(&block)
|
331
|
+
@on_subscribe_block = block
|
332
|
+
end
|
333
|
+
|
334
|
+
def on_publish_finished(&block)
|
335
|
+
@on_publish_finished_block = block
|
336
|
+
end
|
337
|
+
|
338
|
+
def on_message(&block)
|
339
|
+
@on_message_block = block
|
340
|
+
end
|
341
|
+
|
342
|
+
def handle_packet(type,flags,length,data)
|
343
|
+
debug "+++ #{MQTT_PACKET_TYPES[type]} flags: #{flags} length: #{length} data: #{data.unpack('H*').first}"
|
344
|
+
case type
|
345
|
+
when CONNACK
|
346
|
+
return_code = data[1].ord
|
347
|
+
if return_code == 0
|
348
|
+
@state = :mqtt_connected
|
349
|
+
session_present = (data[0].ord == 1)
|
350
|
+
@on_connect_block.call(session_present) unless @on_connect_block.nil?
|
351
|
+
|
352
|
+
#sending QoS 1 and Qos2 messages
|
353
|
+
@outgoing_qos1_store.each do |packet_id,m|
|
354
|
+
debug "resending QoS 1 packet #{packet_id} #{m[0]} #{m[1]}"
|
355
|
+
publish_dup(m[0],m[1],m[2],m[3],true,packet_id)
|
356
|
+
end
|
357
|
+
@outgoing_qos2_store.each do |packet_id,m|
|
358
|
+
state = m[4]
|
359
|
+
if state == PUBLISH
|
360
|
+
debug "resending QoS 2 packet PUBLISH #{packet_id} #{m[0]} #{m[1]}"
|
361
|
+
publish_dup(m[0],m[1],m[2],m[3],true,packet_id)
|
362
|
+
elsif state == PUBREL
|
363
|
+
debug "resending QoS 2 packet PUBREL #{packet_id} #{m[0]} #{m[1]}"
|
364
|
+
pubrel(packet_id)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
else
|
368
|
+
@on_mqtt_connect_error_block.call(return_code) unless @on_mqtt_connect_error_block.nil?
|
369
|
+
end
|
370
|
+
|
371
|
+
when PUBLISH
|
372
|
+
qos = (flags & 6) >> 1
|
373
|
+
topic_length = decode_short(data[0..1])
|
374
|
+
topic = data[2..topic_length+1]
|
375
|
+
|
376
|
+
if (qos > 0)
|
377
|
+
packet_id = decode_short(data[topic_length+2..topic_length+3])
|
378
|
+
message_starts_at = 4
|
379
|
+
else
|
380
|
+
packet_id = nil
|
381
|
+
message_starts_at = 2
|
382
|
+
end
|
383
|
+
message = data[topic_length+message_starts_at..-1]
|
384
|
+
|
385
|
+
if qos == 0
|
386
|
+
@on_message_block.call(topic, message, qos, packet_id) unless @on_message_block.nil?
|
387
|
+
elsif qos == 1
|
388
|
+
if @incoming_qos1_store[packet_id].nil?
|
389
|
+
@incoming_qos1_store[packet_id] = true
|
390
|
+
save_everytime
|
391
|
+
@on_message_block.call(topic, message, qos, packet_id) unless @on_message_block.nil?
|
392
|
+
end
|
393
|
+
|
394
|
+
sent = puback(packet_id)
|
395
|
+
if sent
|
396
|
+
@incoming_qos1_store.delete packet_id
|
397
|
+
save_everytime
|
398
|
+
end
|
399
|
+
|
400
|
+
elsif qos == 2
|
401
|
+
pubrec(packet_id)
|
402
|
+
if @incoming_qos2_store[packet_id].nil?
|
403
|
+
@incoming_qos2_store[packet_id] = true
|
404
|
+
save_everytime
|
405
|
+
@on_message_block.call(topic, message, qos, packet_id) unless @on_message_block.nil?
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
when PUBACK
|
410
|
+
packet_id = decode_short(data)
|
411
|
+
if @outgoing_qos1_store.has_key?(packet_id)
|
412
|
+
@outgoing_qos1_store.delete(packet_id)
|
413
|
+
save_everytime()
|
414
|
+
@on_publish_finished_block.call(packet_id) unless @on_publish_finished_block.nil?
|
415
|
+
else
|
416
|
+
debug "WARNING: PUBACK #{packet_id} not found"
|
417
|
+
end
|
418
|
+
|
419
|
+
when PUBREC
|
420
|
+
packet_id = decode_short(data)
|
421
|
+
p = @outgoing_qos2_store[packet_id]
|
422
|
+
unless p.nil?
|
423
|
+
if p[4] == PUBLISH
|
424
|
+
@outgoing_qos2_store[packet_id][4] = PUBREL
|
425
|
+
save_everytime()
|
426
|
+
else
|
427
|
+
debug "WARNING: PUBREC #{packet_id} not in PUBLISH state"
|
428
|
+
end
|
429
|
+
else
|
430
|
+
debug "WARNING: PUBREC #{packet_id} not found"
|
431
|
+
end
|
432
|
+
pubrel(packet_id)
|
433
|
+
|
434
|
+
when PUBREL
|
435
|
+
packet_id = decode_short(data)
|
436
|
+
sent = pubcomp(packet_id)
|
437
|
+
if sent
|
438
|
+
@incoming_qos2_store.delete packet_id
|
439
|
+
save_everytime
|
440
|
+
end
|
441
|
+
|
442
|
+
when PUBCOMP
|
443
|
+
packet_id = decode_short(data)
|
444
|
+
p = @outgoing_qos2_store[packet_id]
|
445
|
+
unless p.nil?
|
446
|
+
if p[4] == PUBREL
|
447
|
+
@outgoing_qos2_store.delete(packet_id)
|
448
|
+
@on_publish_finished_block.call(packet_id) unless @on_publish_finished_block.nil?
|
449
|
+
save_everytime()
|
450
|
+
else
|
451
|
+
debug "WARNING: PUBCOMP #{packet_id} not in PUBLISH state"
|
452
|
+
end
|
453
|
+
else
|
454
|
+
debug "WARNING: PUBCOMP #{packet_id} not found"
|
455
|
+
end
|
456
|
+
|
457
|
+
when SUBACK
|
458
|
+
# for each topic
|
459
|
+
#@on_subscribe_block.call(topic_name, packet_id, ret)
|
460
|
+
|
461
|
+
when PINGREQ
|
462
|
+
pingresp
|
463
|
+
|
464
|
+
when PINGRESP
|
465
|
+
else
|
466
|
+
debug "WARNING: packet type: #{type} is not handled"
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
def save_everytime
|
471
|
+
if @persistence_filename && (@persistence_mode == :save_everytime)
|
472
|
+
save
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
def save
|
477
|
+
if @persistence_filename
|
478
|
+
File.open(@persistence_filename,"w+") do |f|
|
479
|
+
debug "saving state to " + @persistence_filename
|
480
|
+
f.write Marshal.dump([@outgoing_qos1_store,@outgoing_qos2_store,@incoming_qos1_store,@incoming_qos2_store])
|
481
|
+
end
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
def read_bytes(count)
|
486
|
+
buffer = ''
|
487
|
+
while buffer.length != count
|
488
|
+
#TODO rescue
|
489
|
+
chunk = @socket.read(count - buffer.length)
|
490
|
+
if chunk == '' || chunk.nil?
|
491
|
+
raise Mqtt3NormalExitException
|
492
|
+
else
|
493
|
+
buffer += chunk
|
494
|
+
end
|
495
|
+
end
|
496
|
+
return buffer
|
497
|
+
end
|
498
|
+
|
499
|
+
def init_ssl
|
500
|
+
@ssl_cert = File.read(@ssl_cert_file) if @ssl_cert_file
|
501
|
+
@ssl_key = File.read(@ssl_key_file) if @ssl_key_file
|
502
|
+
|
503
|
+
@ssl_context = OpenSSL::SSL::SSLContext.new
|
504
|
+
|
505
|
+
unless @ssl.is_a?(TrueClass)
|
506
|
+
@ssl_context.ssl_version = @ssl
|
507
|
+
end
|
508
|
+
|
509
|
+
@ssl_context.cert = OpenSSL::X509::Certificate.new(@ssl_cert) if @ssl_cert
|
510
|
+
@ssl_context.key = OpenSSL::PKey::RSA.new(@ssl_key, @ssl_passphrase) if @ssl_key
|
511
|
+
@ssl_context.ca_file = @ssl_ca_file if @ssl_ca_file
|
512
|
+
|
513
|
+
@ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
514
|
+
end
|
515
|
+
|
516
|
+
def debug(x)
|
517
|
+
if @debug
|
518
|
+
print Time.now.strftime('%Y.%m.%d %H:%M:%S.%L ')
|
519
|
+
puts x
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
def run
|
524
|
+
#persistence
|
525
|
+
if @persistence_filename
|
526
|
+
if @clean_session
|
527
|
+
if File.exist?(@persistence_filename)
|
528
|
+
debug "removing file " + @persistence_filename
|
529
|
+
File.delete(@persistence_filename)
|
530
|
+
end
|
531
|
+
else
|
532
|
+
if File.exist?(@persistence_filename)
|
533
|
+
@outgoing_qos1_store, @outgoing_qos2_store, @incoming_qos1_store, @incoming_qos2_store = Marshal.load(File.read(@persistence_filename))
|
534
|
+
debug "loading state from #{@persistence_filename} out_QoS1:#{@outgoing_qos1_store.inspect} out_QoS2:#{@outgoing_qos2_store.inspect} in_QoS1: #{@incoming_qos1_store.inspect} in_QoS2: #{@incoming_qos2_store.inspect}"
|
535
|
+
end
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
Fiber.schedule do
|
540
|
+
@fiber_main = Fiber.current
|
541
|
+
#debug 'entering main fiber' + @fiber_main.inspect
|
542
|
+
counter = 0
|
543
|
+
while @reconnect do
|
544
|
+
ret = tcp_connect()
|
545
|
+
if ret.is_a? (Exception)
|
546
|
+
@on_tcp_connect_error_block.call(e,counter) unless @on_tcp_connect_error_block.nil?
|
547
|
+
else
|
548
|
+
@socket = ret
|
549
|
+
@state = :tcp_connected
|
550
|
+
counter = 0
|
551
|
+
debug 'TCP connected'
|
552
|
+
connect
|
553
|
+
|
554
|
+
@fiber_ping = run_fiber_ping
|
555
|
+
|
556
|
+
begin
|
557
|
+
e = read_from_socket_loop()
|
558
|
+
rescue Mqtt3NormalExitException
|
559
|
+
@reconnect = false
|
560
|
+
rescue
|
561
|
+
end
|
562
|
+
|
563
|
+
@fiber_ping.raise(Mqtt3NormalExitException)
|
564
|
+
|
565
|
+
@state = :disconnected
|
566
|
+
@on_disconnect_block.call(e) unless @on_disconnect_block.nil?
|
567
|
+
end
|
568
|
+
|
569
|
+
if @reconnect
|
570
|
+
if @on_reconnect_block
|
571
|
+
@on_reconnect_block.call(counter)
|
572
|
+
counter += 1
|
573
|
+
else
|
574
|
+
if counter > 0
|
575
|
+
sleep counter
|
576
|
+
end
|
577
|
+
|
578
|
+
if counter == 0
|
579
|
+
counter = 1
|
580
|
+
else
|
581
|
+
counter *= 2
|
582
|
+
counter = 300 if counter > 300
|
583
|
+
end
|
584
|
+
end
|
585
|
+
end
|
586
|
+
end
|
587
|
+
#debug 'exiting main fiber' + @fiber_main.inspect
|
588
|
+
end
|
589
|
+
end
|
590
|
+
|
591
|
+
def stop
|
592
|
+
#puts "sending raise to #{@fiber_main}"
|
593
|
+
@fiber_main.raise(Mqtt3NormalExitException)
|
594
|
+
end
|
595
|
+
|
596
|
+
def run_fiber_ping
|
597
|
+
fiber_ping = Fiber.schedule do
|
598
|
+
#debug 'entering ping fiber' + Fiber.current.inspect
|
599
|
+
begin
|
600
|
+
loop do
|
601
|
+
if @last_packet_sent_at.nil? || @state != :mqtt_connected
|
602
|
+
#debug "sleeping for #{@keepalive_sec} sec"
|
603
|
+
sleep @keepalive_sec
|
604
|
+
else
|
605
|
+
#only send when needed (store time, and adjust sleep with it)
|
606
|
+
while ((t = @last_packet_sent_at + @keepalive_sec - Time.now) >= 0) do
|
607
|
+
#debug "sleeping for #{t} sec"
|
608
|
+
sleep t
|
609
|
+
end
|
610
|
+
pingreq
|
611
|
+
end
|
612
|
+
end
|
613
|
+
rescue Mqtt3NormalExitException
|
614
|
+
end
|
615
|
+
#debug 'exiting ping fiber' + @fiber_ping.inspect
|
616
|
+
end
|
617
|
+
return fiber_ping
|
618
|
+
end
|
619
|
+
|
620
|
+
def tcp_connect
|
621
|
+
begin
|
622
|
+
tcp_socket = TCPSocket.new(@host, @port, connect_timeout: 1)
|
623
|
+
|
624
|
+
if @ssl
|
625
|
+
socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, @ssl_context)
|
626
|
+
socket.sync_close = true
|
627
|
+
# Set hostname on secure socket for Server Name Indication (SNI)
|
628
|
+
#TODO ??? socket.hostname = @host if socket.respond_to?(:hostname=)
|
629
|
+
socket.connect
|
630
|
+
else
|
631
|
+
socket = tcp_socket
|
632
|
+
end
|
633
|
+
|
634
|
+
return socket
|
635
|
+
rescue => e
|
636
|
+
return e
|
637
|
+
end
|
638
|
+
end
|
639
|
+
|
640
|
+
def read_from_socket_loop
|
641
|
+
loop do
|
642
|
+
x = read_bytes(1).ord
|
643
|
+
type = (x & 0xf0) >> 4
|
644
|
+
flags = x & 0x0f
|
645
|
+
|
646
|
+
# Read in the packet length
|
647
|
+
multiplier = 1
|
648
|
+
length = 0
|
649
|
+
pos = 1
|
650
|
+
|
651
|
+
loop do
|
652
|
+
digit = read_bytes(1).ord
|
653
|
+
length += ((digit & 0x7F) * multiplier)
|
654
|
+
multiplier *= 0x80
|
655
|
+
pos += 1
|
656
|
+
break if (digit & 0x80).zero? || pos > 4
|
657
|
+
end
|
658
|
+
|
659
|
+
data = read_bytes(length)
|
660
|
+
handle_packet(type, flags, length, data)
|
661
|
+
end
|
662
|
+
end
|
663
|
+
end
|
metadata
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ruby-mqtt3
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- jsaak
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-05-11 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Using Fibers and Fiber.scheduler, so needs ruby 3
|
14
|
+
email: fake@fake.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/ruby-mqtt3.rb
|
20
|
+
homepage: https://github.com/jsaak/ruby-mqtt3
|
21
|
+
licenses:
|
22
|
+
- MIT
|
23
|
+
metadata: {}
|
24
|
+
post_install_message:
|
25
|
+
rdoc_options: []
|
26
|
+
require_paths:
|
27
|
+
- lib
|
28
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ">="
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
requirements: []
|
39
|
+
rubygems_version: 3.2.15
|
40
|
+
signing_key:
|
41
|
+
specification_version: 4
|
42
|
+
summary: Ruby implementation of MQTT 3.1.1 protocol
|
43
|
+
test_files: []
|