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.
Files changed (31) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +51 -0
  3. data/CODE_OF_CONDUCT.md +13 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE +201 -0
  6. data/README.md +175 -0
  7. data/Rakefile +6 -0
  8. data/aws_iot_device.gemspec +39 -0
  9. data/bin/console +11 -0
  10. data/bin/setup +7 -0
  11. data/lib/aws_iot_device.rb +7 -0
  12. data/lib/aws_iot_device/mqtt_adapter.rb +32 -0
  13. data/lib/aws_iot_device/mqtt_adapter/client.rb +139 -0
  14. data/lib/aws_iot_device/mqtt_adapter/mqtt_adapter.rb +139 -0
  15. data/lib/aws_iot_device/mqtt_adapter/ruby_mqtt_adapter.rb +176 -0
  16. data/lib/aws_iot_device/mqtt_shadow_client.rb +6 -0
  17. data/lib/aws_iot_device/mqtt_shadow_client/json_payload_parser.rb +34 -0
  18. data/lib/aws_iot_device/mqtt_shadow_client/mqtt_manager.rb +135 -0
  19. data/lib/aws_iot_device/mqtt_shadow_client/shadow_action_manager.rb +235 -0
  20. data/lib/aws_iot_device/mqtt_shadow_client/shadow_client.rb +60 -0
  21. data/lib/aws_iot_device/mqtt_shadow_client/shadow_topic_manager.rb +50 -0
  22. data/lib/aws_iot_device/mqtt_shadow_client/token_creator.rb +32 -0
  23. data/lib/aws_iot_device/mqtt_shadow_client/topic_builder.rb +50 -0
  24. data/lib/aws_iot_device/version.rb +3 -0
  25. data/samples/mqtt_client_samples/mqtt_client_samples.rb +90 -0
  26. data/samples/shadow_action_samples/sample_shadow_action_update.rb +79 -0
  27. data/samples/shadow_client_samples/samples_shadow_client_delete.rb +73 -0
  28. data/samples/shadow_client_samples/samples_shadow_client_get.rb +74 -0
  29. data/samples/shadow_client_samples/samples_shadow_client_update.rb +81 -0
  30. data/samples/shadow_topic_samples/sample_topic_manager.rb +77 -0
  31. metadata +186 -0
@@ -0,0 +1,6 @@
1
+ require 'aws_iot_device/mqtt_shadow_client/shadow_client'
2
+
3
+ module AwsIotDevice
4
+ module MqttShadowClient
5
+ end
6
+ 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,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