aws_iot_device 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 +7 -0
- data/.gitignore +51 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE +201 -0
- data/README.md +175 -0
- data/Rakefile +6 -0
- data/aws_iot_device.gemspec +39 -0
- data/bin/console +11 -0
- data/bin/setup +7 -0
- data/lib/aws_iot_device.rb +7 -0
- data/lib/aws_iot_device/mqtt_adapter.rb +32 -0
- data/lib/aws_iot_device/mqtt_adapter/client.rb +139 -0
- data/lib/aws_iot_device/mqtt_adapter/mqtt_adapter.rb +139 -0
- data/lib/aws_iot_device/mqtt_adapter/ruby_mqtt_adapter.rb +176 -0
- data/lib/aws_iot_device/mqtt_shadow_client.rb +6 -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 +135 -0
- data/lib/aws_iot_device/mqtt_shadow_client/shadow_action_manager.rb +235 -0
- data/lib/aws_iot_device/mqtt_shadow_client/shadow_client.rb +60 -0
- data/lib/aws_iot_device/mqtt_shadow_client/shadow_topic_manager.rb +50 -0
- data/lib/aws_iot_device/mqtt_shadow_client/token_creator.rb +32 -0
- data/lib/aws_iot_device/mqtt_shadow_client/topic_builder.rb +50 -0
- data/lib/aws_iot_device/version.rb +3 -0
- data/samples/mqtt_client_samples/mqtt_client_samples.rb +90 -0
- data/samples/shadow_action_samples/sample_shadow_action_update.rb +79 -0
- data/samples/shadow_client_samples/samples_shadow_client_delete.rb +73 -0
- data/samples/shadow_client_samples/samples_shadow_client_get.rb +74 -0
- data/samples/shadow_client_samples/samples_shadow_client_update.rb +81 -0
- data/samples/shadow_topic_samples/sample_topic_manager.rb +77 -0
- metadata +186 -0
@@ -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,135 @@
|
|
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 :host
|
14
|
+
|
15
|
+
attr_accessor :port
|
16
|
+
|
17
|
+
attr_accessor :ssl
|
18
|
+
|
19
|
+
def initialize(*args)
|
20
|
+
@client = create_mqtt_adapter(*args)
|
21
|
+
@mutex_publish = Mutex.new()
|
22
|
+
@mutex_subscribe = Mutex.new()
|
23
|
+
@mutex_unsubscribe = Mutex.new()
|
24
|
+
@ssl_configured = false
|
25
|
+
|
26
|
+
if args.last.is_a?(Hash)
|
27
|
+
attr = args.pop
|
28
|
+
attr.each_pair do |k, v|
|
29
|
+
self.send("#{k}=", v)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
if need_ssl_configure?
|
34
|
+
@client.set_tls_ssl_context(@ca_file, @cert, @key)
|
35
|
+
@ssl_configured = true
|
36
|
+
end
|
37
|
+
|
38
|
+
### Set the on_message's callback
|
39
|
+
@client.on_message = Proc.new do |message|
|
40
|
+
on_message_callback(message)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def cert_file=(path)
|
45
|
+
@cert = path
|
46
|
+
end
|
47
|
+
|
48
|
+
def key_file=(path)
|
49
|
+
@key = path
|
50
|
+
end
|
51
|
+
|
52
|
+
def ca_file=(path)
|
53
|
+
@ca_file = path
|
54
|
+
end
|
55
|
+
|
56
|
+
def client_id
|
57
|
+
@client.client_id
|
58
|
+
end
|
59
|
+
|
60
|
+
def create_mqtt_adapter(*args)
|
61
|
+
@client = MqttAdapter::Client.new(*args)
|
62
|
+
end
|
63
|
+
|
64
|
+
def on_message_callback(message)
|
65
|
+
puts "Received (with no custom callback registred) : "
|
66
|
+
puts "------------------- Topic: #{message.topic}"
|
67
|
+
puts "------------------- Payload: #{message.payload}"
|
68
|
+
end
|
69
|
+
|
70
|
+
def config_endpoint(host, port)
|
71
|
+
if host.nil? || port.nil?
|
72
|
+
raise "config_endpoint error: either host || port is undefined error"
|
73
|
+
end
|
74
|
+
@host = host
|
75
|
+
@port = port
|
76
|
+
end
|
77
|
+
|
78
|
+
def config_ssl_context(ca_file, key, cert)
|
79
|
+
@ca_file = ca_file
|
80
|
+
@key = key
|
81
|
+
@cert = cert
|
82
|
+
@client.set_tls_ssl_context(ca_file, cert, key)
|
83
|
+
end
|
84
|
+
|
85
|
+
def connect(keep_alive_interval=30, &block)
|
86
|
+
if keep_alive_interval.nil? && keep_alive_interval.is_a(Integer)
|
87
|
+
raise "connect error: keep_alive_interval cannot be a not nil Interger"
|
88
|
+
end
|
89
|
+
|
90
|
+
@client.host=(@host)
|
91
|
+
@client.port=(@port)
|
92
|
+
### Execute a mqtt opration loop in background for time period defined by mqtt_connection_timeout
|
93
|
+
@client.connect(block)
|
94
|
+
end
|
95
|
+
|
96
|
+
def disconnect
|
97
|
+
@client.disconnect
|
98
|
+
end
|
99
|
+
|
100
|
+
def publish(topic, payload="", qos=0, retain=nil)
|
101
|
+
if topic.nil?
|
102
|
+
raise "publish error: topic cannot be nil"
|
103
|
+
end
|
104
|
+
@mutex_publish.synchronize{
|
105
|
+
@client.publish(topic,payload,qos,retain)
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
def subscribe(topic, qos=0, callback=nil)
|
110
|
+
if topic.nil?
|
111
|
+
raise "subscribe error: topic cannot be nil"
|
112
|
+
end
|
113
|
+
ret = false
|
114
|
+
@mutex_subscribe.synchronize {
|
115
|
+
@client.add_callback_filter_topic(topic, callback)
|
116
|
+
@client.subscribe(topic)
|
117
|
+
}
|
118
|
+
end
|
119
|
+
|
120
|
+
def unsubscribe(topic)
|
121
|
+
if topic.nil?
|
122
|
+
raise "unsubscribe error: topic cannot be nil"
|
123
|
+
end
|
124
|
+
@mutex_unsubscribe.synchronize{
|
125
|
+
@client.remove_callback_filter_topic(topic)
|
126
|
+
@client.unsubscribe(topic)
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
def need_ssl_configure?
|
131
|
+
!( @ca_file.nil? || @cert.nil? || @key.nil? ) && @ssl
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,235 @@
|
|
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 allows to execute the AWS actions (get, update, delete)
|
13
|
+
### It allows to manage the time out after an action have been start
|
14
|
+
### Actions request are send on the general actions topic and answer is retreived from accepted/refused/delta topics
|
15
|
+
|
16
|
+
def initialize(shadow_name, shadow_topic_manager, persistent_subscribe=false)
|
17
|
+
@shadow_name = shadow_name
|
18
|
+
@topic_manager = shadow_topic_manager
|
19
|
+
@payload_parser = JSONPayloadParser.new
|
20
|
+
@is_subscribed = {}
|
21
|
+
@is_subscribed[:get] = false
|
22
|
+
@is_subscribed[:update] = false
|
23
|
+
@is_subscribed[:delete] = false
|
24
|
+
@token_handler = TokenCreator.new(shadow_name, shadow_topic_manager.client_id)
|
25
|
+
@persistent_subscribe = persistent_subscribe
|
26
|
+
@last_stable_version = -1 #Mean no currentely stable
|
27
|
+
@topic_subscribed_callback = {}
|
28
|
+
@topic_subscribed_callback[:get] = nil
|
29
|
+
@topic_subscribed_callback[:update] = nil
|
30
|
+
@topic_subscribed_callback[:delta] = nil
|
31
|
+
@topic_subscribed_task_count = {}
|
32
|
+
@topic_subscribed_task_count[:get] = 0
|
33
|
+
@topic_subscribed_task_count[:update] = 0
|
34
|
+
@topic_subscribed_task_count[:delete] = 0
|
35
|
+
@token_pool = {}
|
36
|
+
@general_action_mutex = Mutex.new
|
37
|
+
@default_callback = Proc.new do |message|
|
38
|
+
do_default_callback(message)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
### The default callback that is called by every actions
|
43
|
+
### It acknowledge the accepted status if action success
|
44
|
+
### Call a specific callback for each actions if it defined have been register previously
|
45
|
+
def do_default_callback(message)
|
46
|
+
@general_action_mutex.synchronize(){
|
47
|
+
topic = message.topic
|
48
|
+
action = parse_action(topic)
|
49
|
+
type = parse_type(topic)
|
50
|
+
payload = message.payload
|
51
|
+
@payload_parser.set_message(payload)
|
52
|
+
if %w(get update delete).include?(action)
|
53
|
+
token = @payload_parser.get_attribute_value("clientToken")
|
54
|
+
if @token_pool.has_key?(token)
|
55
|
+
if type.eql?("accepted")
|
56
|
+
new_version = @payload_parser.get_attribute_value("version")
|
57
|
+
if new_version && new_version >= @last_stable_version
|
58
|
+
type.eql?("delete") ? @last_stable_version = -1 : @last_stable_version = new_version
|
59
|
+
Thread.new { @topic_subscribed_callback[action.to_sym].call(message) } unless @topic_subscribed_callback[action.to_sym].nil?
|
60
|
+
else
|
61
|
+
puts "CATCH AN UPDATE BUT OUTDATED/INVALID VERSION (= #{new_version}) FOR TOKEN #{token}\n"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
@token_pool[token].cancel
|
65
|
+
@token_pool.delete(token)
|
66
|
+
@topic_subscribed_task_count[action.to_sym] -= 1
|
67
|
+
if @topic_subscribed_task_count[action.to_sym] <= 0
|
68
|
+
@topic_subscribed_task_count[action.to_sym] = 0
|
69
|
+
unless @persistent_subscribe
|
70
|
+
@topic_manager.shadow_topic_unsubscribe(@shadow_name, action)
|
71
|
+
@is_subscribed[action.to_sym] = false
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
elsif %w(delta).include?(action)
|
76
|
+
new_version = @payload_parser.get_attribute_value("version")
|
77
|
+
if new_version && new_version >= @last_stable_version
|
78
|
+
@last_stable_version = new_version
|
79
|
+
Thread.new { @topic_subscribed_callback[action.to_sym].call(message) } if @topic_subscribed_callback[action.to_sym]
|
80
|
+
else
|
81
|
+
puts "CATCH A DELTA BUT OUTDATED/INVALID VERSION (= #{new_version})\n"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
### Should cancel the token after a preset time interval
|
88
|
+
def timeout_manager(action_name, token)
|
89
|
+
@general_action_mutex.synchronize(){
|
90
|
+
if @token_pool.has_key?(token)
|
91
|
+
action = action_name.to_sym
|
92
|
+
@token_pool.delete(token)
|
93
|
+
puts "The #{action_name} request with the token #{token} has timed out!\n"
|
94
|
+
@topic_subscribed_task_count[action] -= 1
|
95
|
+
unless @topic_subscribed_task_count[action] <= 0
|
96
|
+
@topic_subscribed_task_count[action] = 0
|
97
|
+
unless @persistent_subscribe
|
98
|
+
@topic_manager.shadow_topic_unsubscribe(@shadow_name, action)
|
99
|
+
@is_subscribed[action.to_sym] = false
|
100
|
+
end
|
101
|
+
end
|
102
|
+
unless @topic_subscribed_callback[action].blank?
|
103
|
+
puts "Shadow request with token: #{token} has timed out."
|
104
|
+
@topic_subscribed_callback[action].call("REQUEST TIME OUT", "timeout", token)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
### Send and publish packet with an empty payload contains in a valid JSON format.
|
111
|
+
### A unique token is generate and send in the packet in order to trace the action.
|
112
|
+
### Subscribe to the two get/accepted and get/rejected of the coresponding shadow.
|
113
|
+
### If the request is accpeted, the answer would be send on the get/accepted topic.
|
114
|
+
### It contains all the details of the shadow state in JSON document.
|
115
|
+
### A specific callback in Proc could be send parameter.
|
116
|
+
### Before exit, the function start a timer count down in the separate thread.
|
117
|
+
### If the time ran out, the timer_handler function is called and the get action is cancelled using the token.
|
118
|
+
###
|
119
|
+
### Parameter:
|
120
|
+
### > callback: the Proc to execute when the answer to th get request would be received.
|
121
|
+
### It should accept three different paramter:
|
122
|
+
### - payload : the answer content
|
123
|
+
### - response_status : among ['accepted', 'refused', 'delta']
|
124
|
+
### - token : the token assoicate to the get request
|
125
|
+
###
|
126
|
+
### > timeout: the period after which the request should be canceled and timer_handler should be call
|
127
|
+
###
|
128
|
+
### Returns :
|
129
|
+
### > the token associate to the current action (which also store in @token_pool)
|
130
|
+
|
131
|
+
def shadow_get(callback=nil, timeout=5)
|
132
|
+
current_token = ""
|
133
|
+
json_payload = ""
|
134
|
+
timer = Timers::Group.new
|
135
|
+
@general_action_mutex.synchronize(){
|
136
|
+
@topic_subscribed_callback[:get] = callback
|
137
|
+
@topic_subscribed_task_count[:get] += 1
|
138
|
+
current_token = @token_handler.create_next_token
|
139
|
+
timer.after(timeout){ timeout_manager(:get, current_token) }
|
140
|
+
@payload_parser.set_attribute_value("clientToken",current_token)
|
141
|
+
json_payload = @payload_parser.get_json
|
142
|
+
unless @is_subscribed[:get]
|
143
|
+
@topic_manager.shadow_topic_subscribe(@shadow_name, "get", @default_callback)
|
144
|
+
@is_subscribed[:get] = true
|
145
|
+
end
|
146
|
+
@topic_manager.shadow_topic_publish(@shadow_name, "get", json_payload)
|
147
|
+
@token_pool[current_token] = timer
|
148
|
+
Thread.new{ timer.wait }
|
149
|
+
current_token
|
150
|
+
}
|
151
|
+
end
|
152
|
+
|
153
|
+
def shadow_update(payload, callback, timeout)
|
154
|
+
current_token = Symbol
|
155
|
+
timer = Timers::Group.new
|
156
|
+
json_payload = ""
|
157
|
+
@general_action_mutex.synchronize(){
|
158
|
+
if callback.is_a?(Proc)
|
159
|
+
@topic_subscribed_callback[:update] = callback
|
160
|
+
end
|
161
|
+
@topic_subscribed_task_count[:update] += 1
|
162
|
+
current_token = @token_handler.create_next_token
|
163
|
+
timer.after(timeout){ timeout_manager(:update, current_token) }
|
164
|
+
@payload_parser.set_message(payload)
|
165
|
+
@payload_parser.set_attribute_value("clientToken",current_token)
|
166
|
+
json_payload = @payload_parser.get_json
|
167
|
+
unless @is_subscribed[:update]
|
168
|
+
@topic_manager.shadow_topic_subscribe(@shadow_name, "update", @default_callback)
|
169
|
+
@is_subscribed[:update] = true
|
170
|
+
end
|
171
|
+
@topic_manager.shadow_topic_publish(@shadow_name, "update", json_payload)
|
172
|
+
@token_pool[current_token] = timer
|
173
|
+
Thread.new{ timer.wait }
|
174
|
+
current_token
|
175
|
+
}
|
176
|
+
end
|
177
|
+
|
178
|
+
def shadow_delete(callback, timeout)
|
179
|
+
current_token = Symbol
|
180
|
+
timer = Timers::Group.new
|
181
|
+
json_payload = ""
|
182
|
+
@general_action_mutex.synchronize(){
|
183
|
+
if callback.is_a?(Proc)
|
184
|
+
@topic_subscribed_callback[:delete] = callback
|
185
|
+
end
|
186
|
+
@topic_subscribed_task_count[:delete] += 1
|
187
|
+
current_token = @token_handler.create_next_token
|
188
|
+
timer.after(timeout){ timeout_manager(:delete, current_token) }
|
189
|
+
@payload_parser.set_attribute_value("clientToken",current_token)
|
190
|
+
json_payload = @payload_parser.get_json
|
191
|
+
unless @is_subscribed[:delete]
|
192
|
+
@topic_manager.shadow_topic_subscribe(@shadow_name, "delete", @default_callback)
|
193
|
+
@is_subscribed[:delete] = true
|
194
|
+
end
|
195
|
+
@topic_manager.shadow_topic_publish(@shadow_name, "delete", json_payload)
|
196
|
+
@token_pool[current_token] = timer
|
197
|
+
Thread.new{ timer.wait }
|
198
|
+
current_token
|
199
|
+
}
|
200
|
+
end
|
201
|
+
|
202
|
+
def register_shadow_delta_callback(callback)
|
203
|
+
@general_action_mutex.synchronize(){
|
204
|
+
@topic_subscribed_callback[:delta] = callback
|
205
|
+
@topic_manager.shadow_topic_subscribe(@shadow_name, "delta", @default_callback)
|
206
|
+
}
|
207
|
+
end
|
208
|
+
|
209
|
+
def remove_shadow_delta_callback
|
210
|
+
@general_action_mutex.synchronize(){
|
211
|
+
@topic_subscribe_callback.delete[:delta]
|
212
|
+
@topic_manager.shadow_topic_unsubscribe(@shadow_name, "delta")
|
213
|
+
}
|
214
|
+
end
|
215
|
+
|
216
|
+
private
|
217
|
+
|
218
|
+
def parse_shadow_name(topic)
|
219
|
+
topic.split('/')[2]
|
220
|
+
end
|
221
|
+
|
222
|
+
def parse_action(topic)
|
223
|
+
if topic.split('/')[5] == "delta"
|
224
|
+
topic.split('/')[5]
|
225
|
+
else
|
226
|
+
topic.split('/')[4]
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def parse_type(topic)
|
231
|
+
topic.split('/')[5]
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'aws_iot_device/mqtt_shadow_client/mqtt_manager'
|
2
|
+
require 'aws_iot_device/mqtt_shadow_client/shadow_topic_manager'
|
3
|
+
require 'aws_iot_device/mqtt_shadow_client/shadow_action_manager'
|
4
|
+
|
5
|
+
module AwsIotDevice
|
6
|
+
module MqttShadowClient
|
7
|
+
class ShadowClient
|
8
|
+
attr_accessor :action_manager
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@mqtt_client = MqttManager.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def connect
|
15
|
+
@mqtt_client.connect
|
16
|
+
end
|
17
|
+
|
18
|
+
def topic_manager
|
19
|
+
@topic_manager = ShadowTopicManager.new(@mqtt_client)
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_shadow_handler_with_name(shadow_name, is_persistent_subscribe=false)
|
23
|
+
topic_manager
|
24
|
+
@action_manager = ShadowActionManager.new(shadow_name, @topic_manager, is_persistent_subscribe)
|
25
|
+
end
|
26
|
+
|
27
|
+
def get_shadow(callback=nil, timeout=5)
|
28
|
+
@action_manager.shadow_get(callback, timeout)
|
29
|
+
end
|
30
|
+
|
31
|
+
def update_shadow(payload, callback=nil, timeout=5)
|
32
|
+
@action_manager.shadow_update(payload, callback, timeout)
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete_shadow(callback=nil, timeout=5)
|
36
|
+
@action_manager.shadow_delete(callback, timeout)
|
37
|
+
end
|
38
|
+
|
39
|
+
def register_delta_callback(callback)
|
40
|
+
@action_manager.register_shadow_delta_callback(callback)
|
41
|
+
end
|
42
|
+
|
43
|
+
def remove_shadow_delta_callback
|
44
|
+
@action_manager.remove_shadow_delta_callback
|
45
|
+
end
|
46
|
+
|
47
|
+
def disconnect
|
48
|
+
@mqtt_client.disconnect
|
49
|
+
end
|
50
|
+
|
51
|
+
def configure_endpoint(host,port)
|
52
|
+
@mqtt_client.config_endpoint(host,port)
|
53
|
+
end
|
54
|
+
|
55
|
+
def configure_credentials(ca_file, key, cert)
|
56
|
+
@mqtt_client.config_ssl_context(ca_file, key, cert)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|