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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/ruby-mqtt3.rb +663 -0
  3. 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: []