mqtt-sub_handler 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +74 -0
- data/lib/mqtt/sub_handler.rb +104 -24
- data/lib/mqtt/subscription_classes.rb +53 -50
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aaf64955622e7422caca9dab6565f9ae9c80d11c
|
4
|
+
data.tar.gz: 804a85d87e0e22ac033342b96490d7e833143986
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6876a2eb48079b48c30e3cdd8d9156c3fb57f11c63f63b1c1eadde490f96b2a9d41aafe9fd76dacd154866789692d8656701c41322decbf1f8b0930747481ff9
|
7
|
+
data.tar.gz: 3ab96f2b0af4d2f3bc0ce230f9d5f3d9bae4ba90447c4966c3d4b2d9fdf7af21164cb0ac3124805956d3c026f5948e82466fafc688f205c74c44a98554def34d
|
data/README.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
|
2
|
+
**Important note:**
|
3
|
+
*Right now, I (Xasin) am still in the process of figuring out how to properly set up gems.
|
4
|
+
This is my first one, and it has only recently (11.02.18) been published.*
|
5
|
+
|
6
|
+
*I'm working on documenting the code, so if you can't figure out how to get it running, come back in a few days or make an issue explaining your problem!*
|
7
|
+
|
8
|
+
---
|
9
|
+
|
10
|
+
# A topic-based, asynchronous MQTT handler
|
11
|
+
That's right, finally there's a *mostly* stable, connection-loss resistant MQTT handler out there that runs fully asynchronously.
|
12
|
+
What's even nicer is that you can attach (and detach!) callbacks to *any* topic you want - including wildcards!
|
13
|
+
Any topic can have as many callbacks as you want too, so don't worry about that!
|
14
|
+
|
15
|
+
## That's great, but how to get it started?
|
16
|
+
The main code is simple:
|
17
|
+
```ruby
|
18
|
+
require 'mqtt/sub_handler'
|
19
|
+
|
20
|
+
myClient = MQTT::Client.new(ADDRESS_OR_PARAMETERS); # See the "mqtt" gem for possible options!
|
21
|
+
mqttSubHandler = MQTT::SubHandler.new(myClient); # Create a handler with a mqtt class
|
22
|
+
|
23
|
+
mqttSubHandler = MQTT::SubHandler.new('mqtts://Password:Username@Address') # Or use a string!
|
24
|
+
mqttSubHandler = MQTT.Eclipse(); # Or use the quick shortcut to 'iot.eclipse.org' for testing!
|
25
|
+
```
|
26
|
+
### Subscribing
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
mySub = mqttSubHandler.subscribe_to "Any/Topic/You/Want" do |data, [topicMatch]|
|
30
|
+
puts "I got some data: #{data}";
|
31
|
+
end
|
32
|
+
# Yup, it's that simple. The callback will be stored, and will run whenever data is received.
|
33
|
+
|
34
|
+
# You need to get rid of your subscription?
|
35
|
+
# Sure thing!
|
36
|
+
|
37
|
+
mqttSubHandler.unregister_subscription(mySub); # That'll remove the callback, and unsubscribe!
|
38
|
+
# Don't worry about breaking other subscriptions. The code checks if any other callbacks are attached to the topic in question!
|
39
|
+
```
|
40
|
+
|
41
|
+
#### Wildcard subscriptions
|
42
|
+
Wildcard subscriptions are also accepted with this code, and, may I say, are quite useful too!
|
43
|
+
```ruby
|
44
|
+
mqttSubHandler.subscribe_to "A/Wildcard/+/Topic/+" do |data, topicList|
|
45
|
+
puts "I got some data: #{data}"
|
46
|
+
puts "The first + was: #{topicList[0]}"
|
47
|
+
puts "The second one was: #{topicList[1]}"
|
48
|
+
end
|
49
|
+
```
|
50
|
+
Note: This also works with the "#" wildcard. Every following topic "branch" becomes a new array element!
|
51
|
+
|
52
|
+
### Publishing
|
53
|
+
```ruby
|
54
|
+
# Pushing gets easy, too:
|
55
|
+
mqttSubHandler.publish_to "Any/Topic/You/Want", theData, [qos: 1, retain: false]
|
56
|
+
# Right now, the code ONLY SUPPORTS QOS 0, AND WILL BLOCK!
|
57
|
+
# This is courtesy of the MQTT-Gem though, and nothing I can fix for now.
|
58
|
+
```
|
59
|
+
|
60
|
+
## But ... Wait?
|
61
|
+
Yep, you can wait for data, too!
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
mqttSubHandler.wait_for "The/Waiting/Topic", [timeout: seconds or nil] do |data, [topicMatch]|
|
65
|
+
# Confirm and process data here. The rest of the code will wait!
|
66
|
+
# Return true when you found what you need, or set an optional timeout, and the main code will continue after that.
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
The same goes for the end of the code, if you have nothing else to run but want to keep listening:
|
71
|
+
```ruby
|
72
|
+
mqttSubHandler.lockAndListen();
|
73
|
+
# This here also traps SIGINT, so you get a clean exit!
|
74
|
+
```
|
data/lib/mqtt/sub_handler.rb
CHANGED
@@ -4,17 +4,34 @@ require 'mqtt'
|
|
4
4
|
|
5
5
|
require 'mqtt/subscription_classes'
|
6
6
|
|
7
|
+
# @author Xasin
|
7
8
|
module MQTT
|
9
|
+
# A shortcut-function to quickly connect to the public Eclipse MQTT Broker.
|
10
|
+
# @return [MQTT::SubHandler] Sub-Handler connected to `iot.eclipse.org`
|
8
11
|
def self.Eclipse()
|
9
12
|
@EclipseMQTT ||= SubHandler.new('iot.eclipse.org');
|
10
13
|
return @EclipseMQTT;
|
11
14
|
end
|
12
15
|
|
16
|
+
|
13
17
|
class SubHandler
|
18
|
+
# Split a Topic into a Topic-Array
|
19
|
+
# @param topicName [String] The string topic which to split
|
20
|
+
# @return [Array<String>] A list of individual topic-branches
|
21
|
+
# @note This function is mainly used for background processing.
|
14
22
|
def self.getTopicSplit(topicName)
|
15
23
|
return topicName.scan(/[^\/]+/);
|
16
24
|
end
|
17
25
|
|
26
|
+
# Match a topic string to a topic pattern
|
27
|
+
# @param receivedTopicString [String] The string (as
|
28
|
+
# returned by MQTT.get) to compare
|
29
|
+
# @param topicPattern [Array<String>] The Topic-Array (as
|
30
|
+
# returned by .getTopicSplit) to compare against
|
31
|
+
# @return [nil, Array<String>] Nil if no match was found.
|
32
|
+
# An Array of matched wildcard topic branches (can be empty) when
|
33
|
+
# successfully matched
|
34
|
+
# @note (see .getTopicSplit)
|
18
35
|
def self.getTopicMatch(receivedTopicString, topicPattern)
|
19
36
|
receivedTopicList = getTopicSplit receivedTopicString;
|
20
37
|
|
@@ -40,6 +57,7 @@ class SubHandler
|
|
40
57
|
return nil;
|
41
58
|
end
|
42
59
|
|
60
|
+
# Call all existing callbacks whose topic-list matches `topic`
|
43
61
|
def call_interested(topic, data)
|
44
62
|
topicHasReceivers = false;
|
45
63
|
@callbackList.each do |h|
|
@@ -54,6 +72,7 @@ class SubHandler
|
|
54
72
|
end
|
55
73
|
private :call_interested
|
56
74
|
|
75
|
+
# Handle sending a subscription-message to the server
|
57
76
|
def raw_subscribe_to(topic, qos: 1)
|
58
77
|
begin
|
59
78
|
@conChangeMutex.lock
|
@@ -71,48 +90,82 @@ class SubHandler
|
|
71
90
|
end
|
72
91
|
private :raw_subscribe_to
|
73
92
|
|
93
|
+
# @!group Custom subscription handling
|
94
|
+
|
95
|
+
# Unregister a subscription. Removes it from the callback list and
|
96
|
+
# unsubscribes from the topic if no other subscriptions for it are present.
|
97
|
+
# @param subObject [MQTT::Subscriptions::Subscription]
|
98
|
+
# The subscription-object to remove
|
99
|
+
# @return void
|
74
100
|
def unregister_subscription(subObject)
|
75
|
-
raise ArgumentError, "Object is not a subscription!" unless subObject.is_a? MQTT::Subscription
|
101
|
+
raise ArgumentError, "Object is not a subscription!" unless subObject.is_a? MQTT::Subscriptions::Subscription
|
76
102
|
return unless @callbackList.include? subObject;
|
77
103
|
|
78
104
|
@callbackList.delete(subObject);
|
79
105
|
end
|
106
|
+
# Register a custom subscription, and send a subscription message to the server.
|
107
|
+
# @param subObject [MQTT::Subscriptions::Subscription]
|
108
|
+
# An instance of a MQTT Subscription object
|
109
|
+
# @return void
|
80
110
|
def register_subscription(subObject)
|
81
|
-
raise ArgumentError, "Object is not a subscription!" unless subObject.is_a? MQTT::Subscription
|
111
|
+
raise ArgumentError, "Object is not a subscription!" unless subObject.is_a? MQTT::Subscriptions::Subscription
|
82
112
|
return if @callbackList.include? subObject;
|
83
113
|
|
84
114
|
@callbackList << subObject;
|
85
115
|
raw_subscribe_to(subObject.topic, qos: subObject.qos);
|
86
116
|
end
|
87
117
|
|
118
|
+
# @!group Subscribing
|
119
|
+
|
120
|
+
# Synchronously wait for data.
|
121
|
+
# It waits for a message on `topic`, optionally letting a block
|
122
|
+
# check the data for validity, and optionally aborting after a timeout
|
123
|
+
# @param topic [String] The MQTT-Topic to wait for
|
124
|
+
# @param timeout [nil, Integer] The optional timeout after which to abort
|
125
|
+
# @param qos [nil, Integer] The QoS for this subscription
|
126
|
+
# @return [Boolean] True if the block returned true, False if the code timed-out
|
127
|
+
# @yieldparam data [String] The data received via MQTT
|
128
|
+
# @yieldparam topicList [Array<String>] The wildcard topic branches matched.
|
129
|
+
# @yieldreturn [Boolean] Whether or not the data was sufficient, and capture should be stopped.
|
88
130
|
def wait_for(topic, qos: 1, timeout: nil)
|
89
|
-
|
131
|
+
unless block_given? then
|
132
|
+
raise ArgumentError, "A block for data-processing needs to be passed!"
|
133
|
+
end
|
134
|
+
|
135
|
+
subObject = MQTT::Subscriptions::WaitpointSubscription.new(topic, qos);
|
90
136
|
register_subscription(subObject);
|
91
137
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
unregister_subscription(subObject);
|
99
|
-
return true;
|
100
|
-
end
|
138
|
+
begin
|
139
|
+
Timeout::timeout(timeout) do
|
140
|
+
loop do
|
141
|
+
return_data = subObject.waitpoint.wait()[1];
|
142
|
+
if yield(return_data[0], return_data[1]) then
|
143
|
+
return true;
|
101
144
|
end
|
102
145
|
end
|
103
|
-
rescue Timeout::Error
|
104
|
-
return false;
|
105
|
-
end
|
106
|
-
else
|
107
|
-
return_data = subObject.waitpoint.wait(timeout);
|
108
146
|
end
|
109
|
-
|
110
|
-
|
111
|
-
|
147
|
+
rescue Timeout::Error
|
148
|
+
return false;
|
149
|
+
ensure
|
150
|
+
unregister_subscription(subObject);
|
151
|
+
end
|
112
152
|
end
|
153
|
+
|
154
|
+
# Track data changes for a topic in the background.
|
155
|
+
# With no callback given, the returned object can be used to get the last
|
156
|
+
# received raw data string.
|
157
|
+
# With a callback given, the callback will be called whenever a change in data
|
158
|
+
# is detected.
|
159
|
+
# @param topic [String] The MQTT-Topic to track for data. Can be a Wildcard.
|
160
|
+
# @param qos [nil, Integer] The QoS to use for the subscription
|
161
|
+
# @yieldparam data [String] The new (changed) data received from MQTT.
|
162
|
+
# @yieldreturn [void]
|
163
|
+
# @return [MQTT::Subscriptions::ValueTrackerSubscription]
|
164
|
+
# The tracker-object. Can be used to unsubscribe.
|
165
|
+
# More importantly, `tracker.value` can be used to fetch the last received data.
|
113
166
|
def track(topic, qos: 1, &callback)
|
114
167
|
unless(@trackerHash.has_key? topic)
|
115
|
-
subObject = MQTT::ValueTrackerSubscription.new(topic, qos);
|
168
|
+
subObject = MQTT::Subscriptions::ValueTrackerSubscription.new(topic, qos);
|
116
169
|
register_subscription(subObject);
|
117
170
|
|
118
171
|
@trackerHash[topic] = subObject;
|
@@ -123,15 +176,33 @@ class SubHandler
|
|
123
176
|
return @trackerHash[topic];
|
124
177
|
end
|
125
178
|
alias on_change track
|
179
|
+
|
180
|
+
# Attach a callback to a MQTT Topic or wildcard.
|
181
|
+
# The callback will be saved, and asynchronously executed whenever a message
|
182
|
+
# from a matching topic (including wildcards) is received.
|
183
|
+
# @param topic [String] The MQTT-Topic to subscribe to. Can be a Wildcard.
|
184
|
+
# @param qos [nil, Integer] The QoS for the subscription. Currently not used!
|
185
|
+
# @yieldparam data [String] The raw MQTT data received from the MQTT server
|
186
|
+
# @yieldparam topicList [Array<String>] An array of topic-branches corresponding to wildcard matches.
|
187
|
+
# Can be empty if no wildcard was used!
|
188
|
+
# @yieldreturn [void]
|
189
|
+
# @return [MQTT::Subscriptions::CallbackSubscription] The Subscription-Object corresponding to this callback.
|
190
|
+
# Mainly used by the .unregister_subscription function to unsubscribe.
|
126
191
|
def subscribe_to(topic, qos: 1, &callback)
|
127
|
-
subObject = MQTT::CallbackSubscription.new(topic, qos, callback);
|
192
|
+
subObject = MQTT::Subscriptions::CallbackSubscription.new(topic, qos, callback);
|
128
193
|
register_subscription(subObject);
|
129
194
|
|
130
195
|
return subObject;
|
131
196
|
end
|
132
197
|
alias subscribeTo subscribe_to
|
133
198
|
|
199
|
+
# @!endgroup
|
134
200
|
|
201
|
+
# Publish a message to topic.
|
202
|
+
# @param topic [String] The topic to push to.
|
203
|
+
# @param data [String] The data to be transmitted.
|
204
|
+
# @param qos [nil, Numeric] QoS for the publish. Currently not fully supported by the mqtt gem.
|
205
|
+
# @param retain [nil, Boolean] retain-flag for the publish.
|
135
206
|
def publish_to(topic, data, qos: 1, retain: false)
|
136
207
|
raise ArgumentError, "Wrong symbol in topic: #{topic}" if topic =~ /[#\+]/
|
137
208
|
|
@@ -186,6 +257,9 @@ class SubHandler
|
|
186
257
|
end
|
187
258
|
private :mqtt_resub_thread
|
188
259
|
|
260
|
+
# Pause the main thread and wait for messages.
|
261
|
+
# This is mainly useful when the code has set everything up, but doesn't just want to end.
|
262
|
+
# "INT" is trapped, ensuring a smooth exit on Ctrl-C
|
189
263
|
def lockAndListen()
|
190
264
|
Signal.trap("INT") {
|
191
265
|
exit 0
|
@@ -213,8 +287,14 @@ class SubHandler
|
|
213
287
|
end
|
214
288
|
end
|
215
289
|
end
|
216
|
-
|
217
|
-
|
290
|
+
private :flush_pubqueue
|
291
|
+
|
292
|
+
# Initialize a new MQTT::SubHandler
|
293
|
+
# The handler immediately connects to the server, and begins receciving and sending.
|
294
|
+
# @param mqttClient [String, MQTT::Client] Either a URI to connect to, or a MQTT::Client
|
295
|
+
# The URI can be of the form "mqtts://Password@User:URL:port"
|
296
|
+
# The MQTT client instance can be fully configured, as specified by the MQTT Gem. It must **not** already be connected!
|
297
|
+
def initialize(mqttClient)
|
218
298
|
@callbackList = Array.new();
|
219
299
|
if mqttClient.is_a? String then
|
220
300
|
@mqtt = MQTT::Client.new(mqttClient);
|
@@ -2,74 +2,77 @@
|
|
2
2
|
require 'mqtt/Waitpoint.rb'
|
3
3
|
|
4
4
|
module MQTT
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
5
|
+
module Subscriptions
|
6
|
+
# @abstract Basis for custom MQTT::Subcription objects
|
7
|
+
class Subscription
|
8
|
+
attr_reader :topic
|
9
|
+
attr_reader :qos
|
10
|
+
attr_reader :topic_split
|
11
|
+
|
12
|
+
def initialize(topic, qos)
|
13
|
+
@topic = topic;
|
14
|
+
@topic_split = SubHandler.getTopicSplit(topic);
|
15
|
+
|
16
|
+
@qos = 0;
|
17
|
+
end
|
13
18
|
|
14
|
-
|
19
|
+
def offer(topicList, data) end
|
15
20
|
end
|
16
21
|
|
17
|
-
|
18
|
-
|
22
|
+
class CallbackSubscription < Subscription
|
23
|
+
def initialize(topic, qos, callback)
|
24
|
+
super(topic, qos);
|
19
25
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
end
|
26
|
-
def offer(topicList, data)
|
27
|
-
@callback.call(data, topicList);
|
26
|
+
@callback = callback;
|
27
|
+
end
|
28
|
+
def offer(topicList, data)
|
29
|
+
@callback.call(data, topicList);
|
30
|
+
end
|
28
31
|
end
|
29
|
-
end
|
30
32
|
|
31
|
-
|
32
|
-
|
33
|
+
class WaitpointSubscription < Subscription
|
34
|
+
attr_reader :waitpoint
|
33
35
|
|
34
|
-
|
35
|
-
|
36
|
+
def initialize(topic, qos)
|
37
|
+
super(topic, qos);
|
36
38
|
|
37
|
-
|
38
|
-
|
39
|
+
@waitpoint = Xasin::Waitpoint.new();
|
40
|
+
end
|
39
41
|
|
40
|
-
|
41
|
-
|
42
|
+
def offer(topicList, data)
|
43
|
+
@waitpoint.fire([data, topicList]);
|
44
|
+
end
|
42
45
|
end
|
43
|
-
end
|
44
46
|
|
45
|
-
|
46
|
-
|
47
|
+
class ValueTrackerSubscription < Subscription
|
48
|
+
attr_reader :value
|
47
49
|
|
48
|
-
|
49
|
-
|
50
|
-
|
50
|
+
def initialize(topic, qos = 1)
|
51
|
+
raise ArgumentError, "Tracking of topic wildcards is prohibited! Topic: #{topic}" if topic =~ /[#\+]/
|
52
|
+
super(topic, qos);
|
51
53
|
|
52
|
-
|
54
|
+
@value = nil;
|
53
55
|
|
54
|
-
|
55
|
-
|
56
|
+
@callbackList = Array.new();
|
57
|
+
end
|
56
58
|
|
57
|
-
|
58
|
-
|
59
|
+
def offer(topicList, data)
|
60
|
+
return if data == @value;
|
59
61
|
|
60
|
-
|
61
|
-
|
62
|
+
@callbackList.each do |cb|
|
63
|
+
cb.call(data, @value);
|
64
|
+
end
|
65
|
+
@value = data;
|
62
66
|
end
|
63
|
-
@value = data;
|
64
|
-
end
|
65
67
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
68
|
+
def attach(callback)
|
69
|
+
@callbackList << callback;
|
70
|
+
callback.call(@value, nil) if(@value);
|
71
|
+
return callback;
|
72
|
+
end
|
73
|
+
def detach(callback)
|
74
|
+
@callbackList.delete callback;
|
75
|
+
end
|
73
76
|
end
|
74
77
|
end
|
75
78
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mqtt-sub_handler
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Xasin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-02-
|
11
|
+
date: 2018-02-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mqtt
|
@@ -24,12 +24,14 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 0.5.0
|
27
|
-
description:
|
27
|
+
description: Asynchronous handling of callbacks that can be attached to individual
|
28
|
+
topics, based on the mqtt gem.
|
28
29
|
email:
|
29
30
|
executables: []
|
30
31
|
extensions: []
|
31
32
|
extra_rdoc_files: []
|
32
33
|
files:
|
34
|
+
- README.md
|
33
35
|
- lib/mqtt/Waitpoint.rb
|
34
36
|
- lib/mqtt/sub_handler.rb
|
35
37
|
- lib/mqtt/subscription_classes.rb
|