apiotics_aws_client 1.0.1
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/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE +201 -0
- data/README.md +207 -0
- data/Rakefile +6 -0
- data/apiotics-aws-iot-client-1.0.0.gem +0 -0
- data/apiotics_aws_client.gemspec +40 -0
- data/bin/console +11 -0
- data/bin/setup +7 -0
- data/codeclimate.yml +6 -0
- data/lib/aws_iot_device.rb +8 -0
- data/lib/aws_iot_device/mqtt_adapter.rb +31 -0
- data/lib/aws_iot_device/mqtt_adapter/client.rb +207 -0
- data/lib/aws_iot_device/mqtt_adapter/paho_mqtt_adapter.rb +207 -0
- data/lib/aws_iot_device/mqtt_adapter/ruby_mqtt_adapter.rb +183 -0
- data/lib/aws_iot_device/mqtt_shadow_client.rb +7 -0
- data/lib/aws_iot_device/mqtt_shadow_client/json_payload_parser.rb +34 -0
- data/lib/aws_iot_device/mqtt_shadow_client/mqtt_manager.rb +201 -0
- data/lib/aws_iot_device/mqtt_shadow_client/shadow_action_manager.rb +318 -0
- data/lib/aws_iot_device/mqtt_shadow_client/shadow_client.rb +118 -0
- data/lib/aws_iot_device/mqtt_shadow_client/shadow_topic_manager.rb +75 -0
- data/lib/aws_iot_device/mqtt_shadow_client/token_creator.rb +36 -0
- data/lib/aws_iot_device/mqtt_shadow_client/topic_builder.rb +35 -0
- data/lib/aws_iot_device/version.rb +3 -0
- data/samples/config_shadow.rb +72 -0
- data/samples/mqtt_client_samples/mqtt_client_samples.rb +38 -0
- data/samples/shadow_action_samples/sample_shadow_action_update.rb +25 -0
- data/samples/shadow_client_samples/samples_shadow_client_block.rb +25 -0
- data/samples/shadow_client_samples/samples_shadow_client_delete.rb +24 -0
- data/samples/shadow_client_samples/samples_shadow_client_description.rb +64 -0
- data/samples/shadow_client_samples/samples_shadow_client_get.rb +23 -0
- data/samples/shadow_client_samples/samples_shadow_client_getting_started.rb +22 -0
- data/samples/shadow_client_samples/samples_shadow_client_update.rb +30 -0
- data/samples/shadow_topic_samples/sample_topic_manager.rb +26 -0
- metadata +216 -0
@@ -0,0 +1,183 @@
|
|
1
|
+
require 'mqtt'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module AwsIotDevice
|
5
|
+
module MqttAdapter
|
6
|
+
class RubyMqttAdapter
|
7
|
+
|
8
|
+
attr_reader :client_id
|
9
|
+
|
10
|
+
attr_accessor :filtered_topics
|
11
|
+
|
12
|
+
def initialize(*args)
|
13
|
+
@client = MQTT::Client.new(*args)
|
14
|
+
@filtered_topics = {}
|
15
|
+
@client_id = ""
|
16
|
+
@client_id = generate_client_id
|
17
|
+
end
|
18
|
+
|
19
|
+
def client_id
|
20
|
+
@client_id
|
21
|
+
end
|
22
|
+
|
23
|
+
def publish(topic, payload='', retain=false, qos=0)
|
24
|
+
@client.publish(topic, payload, retain, qos)
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_client(*args)
|
28
|
+
@client = MQTT::Client.new(*args)
|
29
|
+
end
|
30
|
+
|
31
|
+
def connect(*args, &block)
|
32
|
+
@client = create_client(*args) if @client.nil?
|
33
|
+
@client.connect(&block)
|
34
|
+
loop_start
|
35
|
+
end
|
36
|
+
|
37
|
+
def generate_client_id(prefix='ruby', lenght=16)
|
38
|
+
charset = Array('A'..'Z') + Array('a'..'z') + Array('0'..'9')
|
39
|
+
@client_id << prefix << Array.new(lenght) { charset.sample }.join
|
40
|
+
end
|
41
|
+
|
42
|
+
def ssl_context
|
43
|
+
@client.ssl_context
|
44
|
+
end
|
45
|
+
|
46
|
+
def disconnect(send_msg=true)
|
47
|
+
@client.disconnect(send_msg)
|
48
|
+
end
|
49
|
+
|
50
|
+
def connected?
|
51
|
+
@client.connected?
|
52
|
+
end
|
53
|
+
|
54
|
+
def subscribe(topics, qos=0)
|
55
|
+
@client.subscribe(topics, qos)
|
56
|
+
end
|
57
|
+
|
58
|
+
def get(topic=nil, &block)
|
59
|
+
@client.get(topic, &block)
|
60
|
+
end
|
61
|
+
|
62
|
+
def get_packet(topic=nil, &block)
|
63
|
+
@client.get_packet(topic, &block)
|
64
|
+
end
|
65
|
+
|
66
|
+
def queue_empty?
|
67
|
+
@client.queue_empty?
|
68
|
+
end
|
69
|
+
|
70
|
+
def queue_length
|
71
|
+
@client.queue_length
|
72
|
+
end
|
73
|
+
|
74
|
+
def unsubscribe(*topics)
|
75
|
+
@client.unsubscribe(*topics)
|
76
|
+
end
|
77
|
+
|
78
|
+
def host
|
79
|
+
@client.host
|
80
|
+
end
|
81
|
+
|
82
|
+
def host=(host)
|
83
|
+
@client.host = host
|
84
|
+
end
|
85
|
+
|
86
|
+
def port
|
87
|
+
@client.port
|
88
|
+
end
|
89
|
+
|
90
|
+
def port=(port)
|
91
|
+
@client.port = port
|
92
|
+
end
|
93
|
+
|
94
|
+
def ssl=(ssl)
|
95
|
+
@client.ssl = ssl
|
96
|
+
end
|
97
|
+
|
98
|
+
def set_tls_ssl_context(ca_cert, cert=nil, key=nil)
|
99
|
+
@client.ssl ||= true
|
100
|
+
@client.ssl_context
|
101
|
+
@client.cert_file = cert
|
102
|
+
@client.key_file = key
|
103
|
+
@client.ca_file = ca_cert
|
104
|
+
end
|
105
|
+
|
106
|
+
############### Custom Features #################
|
107
|
+
|
108
|
+
def loop_start
|
109
|
+
Thread.new{ loop_forever }
|
110
|
+
end
|
111
|
+
|
112
|
+
def loop_stop(thread)
|
113
|
+
thread.join
|
114
|
+
end
|
115
|
+
|
116
|
+
def loop_forever
|
117
|
+
loop do
|
118
|
+
mqtt_loop
|
119
|
+
sleep 0.005
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def mqtt_loop
|
124
|
+
loop_read
|
125
|
+
loop_write
|
126
|
+
loop_misc
|
127
|
+
end
|
128
|
+
|
129
|
+
def loop_read(max_message=10)
|
130
|
+
counter_message = 0
|
131
|
+
while !@client.queue_empty? && counter_message <= max_message
|
132
|
+
message = get_packet
|
133
|
+
### Fitlering message if matching to filtered topic
|
134
|
+
topic = message.topic
|
135
|
+
if @filtered_topics.key?(topic)
|
136
|
+
callback = @filtered_topics[topic]
|
137
|
+
callback.call(message)
|
138
|
+
else
|
139
|
+
on_message_callback(message)
|
140
|
+
end
|
141
|
+
counter_message += 1
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def loop_write
|
146
|
+
puts "loop_write is unavailable for ruby-mqtt client's"
|
147
|
+
end
|
148
|
+
|
149
|
+
def loop_misc
|
150
|
+
puts "loop_misc is unavailable for ruby-mqtt client's"
|
151
|
+
end
|
152
|
+
|
153
|
+
def on_message=(callback)
|
154
|
+
@on_message = callback
|
155
|
+
end
|
156
|
+
|
157
|
+
def on_message(&block)
|
158
|
+
@on_message = block if block_given?
|
159
|
+
@on_message
|
160
|
+
end
|
161
|
+
|
162
|
+
def on_message_callback(message)
|
163
|
+
if @on_message.is_a?(Proc) || @on_message.lambda?
|
164
|
+
@on_message.call(message)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def add_callback_filter_topic(topic, callback)
|
169
|
+
unless callback.nil?
|
170
|
+
@filtered_topics[topic] = callback
|
171
|
+
else
|
172
|
+
@filtered_topic.delete(topic)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def remove_callback_filter_topic(topic)
|
177
|
+
if @filtered_topics.key(topic)
|
178
|
+
@filtered_topics.delete(topic)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'timers'
|
3
|
+
require 'thread'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module AwsIotDevice
|
7
|
+
module MqttShadowClient
|
8
|
+
class JSONPayloadParser
|
9
|
+
### This class acts as Basic JSON parser.
|
10
|
+
### The answer from AWS is in a JSON format.
|
11
|
+
### All different key of the JSON file should be defined as hash key
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@message = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def set_message(message)
|
18
|
+
@message = JSON.parse(message)
|
19
|
+
end
|
20
|
+
|
21
|
+
def get_attribute_value(key)
|
22
|
+
@message[key]
|
23
|
+
end
|
24
|
+
|
25
|
+
def set_attribute_value(key, value)
|
26
|
+
@message[key] = value
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_json
|
30
|
+
@message.to_json
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module AwsIotDevice
|
4
|
+
module MqttShadowClient
|
5
|
+
class MqttManager
|
6
|
+
|
7
|
+
attr_reader :client_id
|
8
|
+
|
9
|
+
attr_accessor :connection_timeout_s
|
10
|
+
|
11
|
+
attr_accessor :mqtt_operation_timeout_s
|
12
|
+
|
13
|
+
attr_accessor :ssl
|
14
|
+
|
15
|
+
def initialize(*args)
|
16
|
+
@client = create_mqtt_adapter(*args)
|
17
|
+
@mqtt_operation_timeout_s = 2
|
18
|
+
@mutex_publish = Mutex.new()
|
19
|
+
@mutex_subscribe = Mutex.new()
|
20
|
+
@mutex_unsubscribe = Mutex.new()
|
21
|
+
end
|
22
|
+
|
23
|
+
def host=(host)
|
24
|
+
@client.host = host
|
25
|
+
end
|
26
|
+
|
27
|
+
def host
|
28
|
+
@client.host
|
29
|
+
end
|
30
|
+
|
31
|
+
def port=(port)
|
32
|
+
@client.port = port
|
33
|
+
end
|
34
|
+
|
35
|
+
def port
|
36
|
+
@client.port
|
37
|
+
end
|
38
|
+
|
39
|
+
def client_id
|
40
|
+
@client.client_id
|
41
|
+
end
|
42
|
+
|
43
|
+
def create_mqtt_adapter(*args)
|
44
|
+
@client = MqttAdapter::Client.new(*args)
|
45
|
+
end
|
46
|
+
|
47
|
+
def config_endpoint(host, port)
|
48
|
+
raise ArgumentError, "configure endpoint either host or port is nil" if host.nil? || port.nil?
|
49
|
+
@client.host = host
|
50
|
+
@client.port = port
|
51
|
+
end
|
52
|
+
|
53
|
+
def config_ssl_context(ca_file, key, cert)
|
54
|
+
self.ca_file = ca_file
|
55
|
+
self.key = key
|
56
|
+
self.cert = cert
|
57
|
+
@client.set_tls_ssl_context(@ca_file, @cert, @key)
|
58
|
+
end
|
59
|
+
|
60
|
+
def connect(*args, &block)
|
61
|
+
### Execute a mqtt opration loop in background for time period defined by mqtt_connection_timeout
|
62
|
+
@client.connect(*args, &block)
|
63
|
+
end
|
64
|
+
|
65
|
+
def disconnect
|
66
|
+
@client.disconnect
|
67
|
+
end
|
68
|
+
|
69
|
+
def publish(topic, payload="", retain=nil, qos=0)
|
70
|
+
raise ArgumentError, "publish topic cannot be nil" if topic.nil?
|
71
|
+
@mutex_publish.synchronize {
|
72
|
+
@client.publish(topic, payload, retain, qos)
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
def subscribe(topic, qos=0, callback=nil)
|
77
|
+
raise ArgumentError, "subscribe topic cannot be nil" if topic.nil?
|
78
|
+
@mutex_subscribe.synchronize {
|
79
|
+
@client.add_callback_filter_topic(topic, callback)
|
80
|
+
@client.subscribe(topic, qos)
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
def subscribe_bunch(*topics)
|
85
|
+
@mutex_subscribe.synchronize {
|
86
|
+
topics.each do |topic|
|
87
|
+
@client.add_callback_filter_topic(topic.first, topic.pop) if !topic[2].nil? && topic[2].is_a?(Proc)
|
88
|
+
end
|
89
|
+
@client.subscribe_bunch(topics)
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
def unsubscribe(topic)
|
94
|
+
raise ArgumentError, "unsubscribe topic cannot be nil" if topic.nil?
|
95
|
+
@mutex_unsubscribe.synchronize{
|
96
|
+
@client.remove_callback_filter_topic(topic)
|
97
|
+
@client.unsubscribe(topic)
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
def unsubscribe_bunch(*topics)
|
102
|
+
@mutex_unsubscribe.synchronize {
|
103
|
+
topics.each do |topic|
|
104
|
+
@client.remove_callback_filter_topic(topic)
|
105
|
+
end
|
106
|
+
@client.unsubscribe_bunch(topics)
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
def on_connack=(callback)
|
111
|
+
@client.on_connack = callback if paho_client?
|
112
|
+
end
|
113
|
+
|
114
|
+
def on_suback=(callback)
|
115
|
+
@client.on_suback = callback if paho_client?
|
116
|
+
end
|
117
|
+
|
118
|
+
def on_unsuback=(callback)
|
119
|
+
@client.on_unsuback = callback if paho_client?
|
120
|
+
end
|
121
|
+
|
122
|
+
def on_puback=(callback)
|
123
|
+
@client.on_puback = callback if paho_client?
|
124
|
+
end
|
125
|
+
|
126
|
+
def on_pubrec=(callback)
|
127
|
+
@client.on_pubrec = callback if paho_client?
|
128
|
+
end
|
129
|
+
|
130
|
+
def on_pubrel=(callback)
|
131
|
+
@client.on_pubrel = callback if paho_client?
|
132
|
+
end
|
133
|
+
|
134
|
+
def on_pubcomp=(callback)
|
135
|
+
@client.on_pubcomp = callback if paho_client?
|
136
|
+
end
|
137
|
+
|
138
|
+
def on_message=(callback)
|
139
|
+
@client.on_message = callback
|
140
|
+
end
|
141
|
+
|
142
|
+
def on_connack(&block)
|
143
|
+
@client.on_connack(&block) if paho_client?
|
144
|
+
end
|
145
|
+
|
146
|
+
def on_suback(&block)
|
147
|
+
@client.on_suback(&block) if paho_client?
|
148
|
+
end
|
149
|
+
|
150
|
+
def on_unsuback(&block)
|
151
|
+
@client.on_unsuback(&block) if paho_client?
|
152
|
+
end
|
153
|
+
|
154
|
+
def on_puback(&block)
|
155
|
+
@client.on_puback(&block) if paho_client?
|
156
|
+
end
|
157
|
+
|
158
|
+
def on_pubrec(&block)
|
159
|
+
@client.on_pubrec(&block) if paho_client?
|
160
|
+
end
|
161
|
+
|
162
|
+
def on_pubrel(&block)
|
163
|
+
@client.on_pubrel(&block) if paho_client?
|
164
|
+
end
|
165
|
+
|
166
|
+
def on_pubcomp(&block)
|
167
|
+
@client.on_pubcomp(&block) if paho_client?
|
168
|
+
end
|
169
|
+
|
170
|
+
def on_message(&block)
|
171
|
+
@client.on_message(&block)
|
172
|
+
end
|
173
|
+
|
174
|
+
def add_topic_callback(topic, callback, &block)
|
175
|
+
@client.add_callback_filter_topic(topic, callback, &block)
|
176
|
+
end
|
177
|
+
|
178
|
+
def remove_topic_callback(topic)
|
179
|
+
@client.remove_callback_filter_topic(topic)
|
180
|
+
end
|
181
|
+
|
182
|
+
def paho_client?
|
183
|
+
@client.adapter.class == MqttAdapter::PahoMqttAdapter
|
184
|
+
end
|
185
|
+
|
186
|
+
private
|
187
|
+
|
188
|
+
def cert=(path)
|
189
|
+
@cert = path
|
190
|
+
end
|
191
|
+
|
192
|
+
def key=(path)
|
193
|
+
@key = path
|
194
|
+
end
|
195
|
+
|
196
|
+
def ca_file=(path)
|
197
|
+
@ca_file = path
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
@@ -0,0 +1,318 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'timers'
|
3
|
+
require 'thread'
|
4
|
+
require 'json'
|
5
|
+
require 'aws_iot_device/mqtt_shadow_client/token_creator'
|
6
|
+
require 'aws_iot_device/mqtt_shadow_client/json_payload_parser'
|
7
|
+
|
8
|
+
module AwsIotDevice
|
9
|
+
module MqttShadowClient
|
10
|
+
class ShadowActionManager
|
11
|
+
### This the main AWS action manager
|
12
|
+
### It enables the AWS IoT actions (get, update, delete)
|
13
|
+
### It enables the time control the time out after an action have been start
|
14
|
+
### Actions requests are send on the general actions topic and answer is retreived from accepted/refused/delta topics
|
15
|
+
|
16
|
+
attr_accessor :logger
|
17
|
+
|
18
|
+
def initialize(shadow_name, mqtt_client, persistent_subscribe=false)
|
19
|
+
@shadow_name = shadow_name
|
20
|
+
@topic_manager = ShadowTopicManager.new(mqtt_client, shadow_name)
|
21
|
+
@payload_parser = JSONPayloadParser.new
|
22
|
+
@is_subscribed = {}
|
23
|
+
@is_subscribed[:get] = false
|
24
|
+
@is_subscribed[:update] = false
|
25
|
+
@is_subscribed[:delete] = false
|
26
|
+
@token_handler = TokenCreator.new(shadow_name, mqtt_client.client_id)
|
27
|
+
@persistent_subscribe = persistent_subscribe
|
28
|
+
@last_stable_version = -1 #Mean no currentely stable
|
29
|
+
@topic_subscribed_callback = {}
|
30
|
+
@topic_subscribed_callback[:get] = nil
|
31
|
+
@topic_subscribed_callback[:update] = nil
|
32
|
+
@topic_subscribed_callback[:delta] = nil
|
33
|
+
@topic_subscribed_task_count = {}
|
34
|
+
@topic_subscribed_task_count[:get] = 0
|
35
|
+
@topic_subscribed_task_count[:update] = 0
|
36
|
+
@topic_subscribed_task_count[:delete] = 0
|
37
|
+
@token_pool = {}
|
38
|
+
@token_callback = {}
|
39
|
+
@task_count_mutex = Mutex.new
|
40
|
+
@token_mutex = Mutex.new
|
41
|
+
@parser_mutex = Mutex.new
|
42
|
+
set_basic_callback
|
43
|
+
end
|
44
|
+
|
45
|
+
### Send and publish packet with an empty payload contains in a valid JSON format.
|
46
|
+
### A unique token is generate and send in the packet in order to trace the action.
|
47
|
+
### Subscribe to the two get/accepted and get/rejected of the coresponding shadow.
|
48
|
+
### If the request is accpeted, the answer would be send on the get/accepted topic.
|
49
|
+
### It contains all the details of the shadow state in JSON document.
|
50
|
+
### A specific callback in Proc could be send parameter.
|
51
|
+
### Before exit, the function start a timer count down in the separate thread.
|
52
|
+
### If the time ran out, the timer_handler function is called and the get action is cancelled using the token.
|
53
|
+
###
|
54
|
+
### Parameter:
|
55
|
+
### > callback: the Proc to execute when the answer to th get request would be received.
|
56
|
+
### It should accept three different paramter:
|
57
|
+
### - payload : the answer content
|
58
|
+
### - response_status : among ['accepted', 'refused', 'delta']
|
59
|
+
### - token : the token assoicate to the get request
|
60
|
+
###
|
61
|
+
### > timeout: the period after which the request should be canceled and timer_handler should be call
|
62
|
+
###
|
63
|
+
### Returns :
|
64
|
+
### > the token associate to the current action (which also store in @token_pool)
|
65
|
+
|
66
|
+
def shadow_get(timeout=5, callback=nil, &block)
|
67
|
+
shadow_action(:get, "", timeout, callback, &block)
|
68
|
+
end
|
69
|
+
|
70
|
+
def shadow_update(payload, timeout=5, callback=nil, &block)
|
71
|
+
shadow_action(:update, payload, timeout, callback, &block)
|
72
|
+
end
|
73
|
+
|
74
|
+
def shadow_delete(timeout=5, callback=nil, &block)
|
75
|
+
shadow_action(:delete, "", timeout, callback, &block)
|
76
|
+
end
|
77
|
+
|
78
|
+
def register_get_callback(callback, &block)
|
79
|
+
register_action_callback(:get, callback, &block)
|
80
|
+
end
|
81
|
+
|
82
|
+
def register_update_callback(callback, &block)
|
83
|
+
register_action_callback(:update, callback, &block)
|
84
|
+
end
|
85
|
+
|
86
|
+
def register_delete_callback(callback, &block)
|
87
|
+
register_action_callback(:delete, callback, &block)
|
88
|
+
end
|
89
|
+
|
90
|
+
def register_shadow_delta_callback(callback, &block)
|
91
|
+
if callback.is_a?(Proc)
|
92
|
+
@topic_subscribed_callback[:delta] = callback
|
93
|
+
elsif block_given?
|
94
|
+
@topic_subscribed_callback[:delta] = block
|
95
|
+
end
|
96
|
+
@topic_manager.shadow_topic_subscribe("delta", @default_callback)
|
97
|
+
end
|
98
|
+
|
99
|
+
def remove_get_callback
|
100
|
+
remove_action_callback(:get)
|
101
|
+
end
|
102
|
+
|
103
|
+
def remove_update_callback
|
104
|
+
remove_action_callback(:update)
|
105
|
+
end
|
106
|
+
|
107
|
+
def remove_delete_callback
|
108
|
+
remove_action_callback(:delete)
|
109
|
+
end
|
110
|
+
|
111
|
+
def remove_shadow_delta_callback
|
112
|
+
@topic_subscribe_callback.delete[:delta]
|
113
|
+
@topic_manager.shadow_topic_unsubscribe("delta")
|
114
|
+
end
|
115
|
+
|
116
|
+
def logger?
|
117
|
+
!@logger.nil? && @logger.is_a?(Logger)
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def shadow_action(action, payload="", timeout=5, callback=nil, &block)
|
123
|
+
current_token = Symbol
|
124
|
+
timer = Timers::Group.new
|
125
|
+
json_payload = ""
|
126
|
+
@token_mutex.synchronize(){
|
127
|
+
current_token = @token_handler.create_next_token
|
128
|
+
}
|
129
|
+
timer.after(timeout){ timeout_manager(action, current_token) }
|
130
|
+
@parser_mutex.synchronize {
|
131
|
+
@payload_parser.set_message(payload) unless payload == ""
|
132
|
+
@payload_parser.set_attribute_value("clientToken", current_token)
|
133
|
+
json_payload = @payload_parser.get_json
|
134
|
+
}
|
135
|
+
handle_subscription(action, timeout) unless @is_subscribed[action]
|
136
|
+
@topic_manager.shadow_topic_publish(action.to_s, json_payload)
|
137
|
+
@task_count_mutex.synchronize {
|
138
|
+
@topic_subscribed_task_count[action] += 1
|
139
|
+
}
|
140
|
+
@token_pool[current_token] = timer
|
141
|
+
register_token_callback(current_token, callback, &block)
|
142
|
+
Thread.new{ timer.wait }
|
143
|
+
current_token
|
144
|
+
end
|
145
|
+
|
146
|
+
### Should cancel the token after a preset time interval
|
147
|
+
def timeout_manager(action_name, token)
|
148
|
+
if @token_pool.has_key?(token)
|
149
|
+
action = action_name.to_sym
|
150
|
+
@token_pool.delete(token)
|
151
|
+
@token_callback.delete(token)
|
152
|
+
@logger.warn("The #{action_name} request with the token #{token} has timed out!\n") if logger?
|
153
|
+
@task_count_mutex.synchronize {
|
154
|
+
@topic_subscribed_task_count[action] -= 1
|
155
|
+
unless @topic_subscribed_task_count[action] <= 0
|
156
|
+
@topic_subscribed_task_count[action] = 0
|
157
|
+
unless @persistent_subscribe
|
158
|
+
@topic_manager.shadow_topic_unsubscribe(action)
|
159
|
+
@is_subscribed[action.to_sym] = false
|
160
|
+
end
|
161
|
+
end
|
162
|
+
}
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def set_basic_callback
|
167
|
+
@default_callback = proc { |message| do_message_callback(message) }
|
168
|
+
|
169
|
+
@topic_manager.on_suback = lambda do |topics|
|
170
|
+
action = @topic_manager.retrieve_action(topics[0])
|
171
|
+
@is_subscribed[action] ||= true unless action.nil?
|
172
|
+
end
|
173
|
+
|
174
|
+
@topic_manager.on_unsuback = lambda do |topics|
|
175
|
+
action = @topic_manager.retrieve_action(topics[0])
|
176
|
+
@is_subscribed[action] = false if action.nil?
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def register_token_callback(token, callback, &block)
|
181
|
+
if callback.is_a?(Proc)
|
182
|
+
@token_callback[token] = callback
|
183
|
+
elsif block_given?
|
184
|
+
@token_callback[token] = block
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def remove_token_callback(token)
|
189
|
+
@token_callback.delete(token)
|
190
|
+
end
|
191
|
+
|
192
|
+
def register_action_callback(action, callback, &block)
|
193
|
+
if callback.is_a?(Proc)
|
194
|
+
@topic_subscribed_callback[action] = callback
|
195
|
+
elsif block_given?
|
196
|
+
@topic_subscribed_callback[action] = block
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def remove_action_callback(action)
|
201
|
+
@topic_subscribed_callback[action] = nil
|
202
|
+
end
|
203
|
+
|
204
|
+
def decresase_task_count(action)
|
205
|
+
@topic_subscribed_task_count[action] -= 1
|
206
|
+
if @topic_subscribed_task_count[action] <= 0
|
207
|
+
@topic_subscribed_task_count[action] = 0
|
208
|
+
unless @persistent_subscribe
|
209
|
+
@topic_manager.shadow_topic_unsubscribe(action.to_s)
|
210
|
+
@is_subscribed[action] = false
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
### The default callback that is called by every actions
|
216
|
+
### It acknowledge the accepted status if action success
|
217
|
+
### Call a specific callback for each actions if it defined have been register previously
|
218
|
+
def do_message_callback(message)
|
219
|
+
topic = message.topic
|
220
|
+
action = parse_action(topic)
|
221
|
+
type = parse_type(topic)
|
222
|
+
payload = message.payload
|
223
|
+
token = nil
|
224
|
+
new_version = -1
|
225
|
+
@parser_mutex.synchronize() {
|
226
|
+
@payload_parser.set_message(payload)
|
227
|
+
new_version = @payload_parser.get_attribute_value("version")
|
228
|
+
token = @payload_parser.get_attribute_value("clientToken")
|
229
|
+
}
|
230
|
+
if %w(get update delete).include?(action)
|
231
|
+
if @token_pool.has_key?(token)
|
232
|
+
@token_pool[token].cancel
|
233
|
+
@token_pool.delete(token)
|
234
|
+
if type.eql?("accepted")
|
235
|
+
do_accepted(message, action.to_sym, token, type, new_version)
|
236
|
+
else
|
237
|
+
do_rejected(token, action, new_version)
|
238
|
+
end
|
239
|
+
@task_count_mutex.synchronize {
|
240
|
+
decresase_task_count(action.to_sym)
|
241
|
+
}
|
242
|
+
end
|
243
|
+
elsif %w(delta).include?(action)
|
244
|
+
do_delta(message, new_version)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def do_accepted(message, action, token, type, new_version)
|
249
|
+
if new_version && new_version >= @last_stable_version
|
250
|
+
@logger.info("The #{action} action with the token #{token} have been accepted.") if logger?
|
251
|
+
type.eql?("delete") ? @last_stable_version = -1 : @last_stable_version = new_version
|
252
|
+
Thread.new do
|
253
|
+
accepted_tasks(message, action, token)
|
254
|
+
end
|
255
|
+
else
|
256
|
+
@logger.warn("CATCH AN ACCEPTED #{action} BUT OUTDATED/INVALID VERSION (= #{new_version})\n") if logger?
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def accepted_tasks(message, action, token)
|
261
|
+
@topic_subscribed_callback[action].call(message) unless @topic_subscribed_callback[action].nil?
|
262
|
+
@token_callback[token].call(message) if @token_callback.has_key?(token)
|
263
|
+
@token_callback.delete(token)
|
264
|
+
end
|
265
|
+
|
266
|
+
def do_rejected(token, action, new_version)
|
267
|
+
if new_version && new_version >= @last_stable_version
|
268
|
+
@logger.info("The #{action} action with the token #{token} have been rejected.") if logger?
|
269
|
+
@token_callback.delete(token)
|
270
|
+
else
|
271
|
+
@logger.warn("CATCH AN REJECTED #{action} BUT OUTDATED/INVALID VERSION (= #{new_version})\n") if logger?
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def do_delta(message, new_version)
|
276
|
+
if new_version && new_version >= @last_stable_version
|
277
|
+
@logger.info("A delta action have been accepted.") if logger?
|
278
|
+
@last_stable_version = new_version
|
279
|
+
Thread.new { @topic_subscribed_callback[:delta].call(message) } unless @topic_subscribed_callback[:delta].nil?
|
280
|
+
else
|
281
|
+
@logger.warn("CATCH A DELTA BUT OUTDATED/INVALID VERSION (= #{new_version})\n") if logger?
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def handle_subscription(action, timeout)
|
286
|
+
@topic_manager.shadow_topic_subscribe(action.to_s, @default_callback)
|
287
|
+
if @topic_manager.paho_client?
|
288
|
+
ref = Time.now + timeout
|
289
|
+
while !@is_subscribed[action] && handle_timeout(ref) do
|
290
|
+
sleep 0.0001
|
291
|
+
end
|
292
|
+
else
|
293
|
+
sleep 2
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def handle_timeout(ref)
|
298
|
+
Time.now <= ref
|
299
|
+
end
|
300
|
+
|
301
|
+
def parse_shadow_name(topic)
|
302
|
+
topic.split('/')[2]
|
303
|
+
end
|
304
|
+
|
305
|
+
def parse_action(topic)
|
306
|
+
if topic.split('/')[5] == "delta"
|
307
|
+
topic.split('/')[5]
|
308
|
+
else
|
309
|
+
topic.split('/')[4]
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
def parse_type(topic)
|
314
|
+
topic.split('/')[5]
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|