fancybox2 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 36eebce6aa26f750e0d2974e2f7ea1a2f029174bddb6d3bb6b8a4adb4e01fd4a
4
+ data.tar.gz: 7f0d67c72d4472d950bbb581a2b2829cea9a0527bdc4377568a3d4293039ccbc
5
+ SHA512:
6
+ metadata.gz: 38a9da60393ba4308c46fca05ef6764111a872128f89cfc62bda02a998df36f64a08f41ef86eedc4108011368cd1eeaf682bbdda1f299ce16bfe2d66dfc075eb
7
+ data.tar.gz: 6600b35da0bf560f6b6f84edaa7f2307b6a49c4ae8e0c831cda9f764e0652b8f25384df236b87f096e19faba44ee86653381decef46c22d00597f65dfc807ea5
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Fancy Pixel S.r.l. All rights reserved.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a
6
+ copy of this software and associated documentation files (the "Software"),
7
+ to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included
14
+ in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,190 @@
1
+ <p align="center">
2
+ <img width="480" src="assets/logo.png"/>
3
+ </p>
4
+
5
+ [![Build Status](https://travis-ci.org/space-bunny/ruby_sdk.svg)](https://travis-ci.org/space-bunny/ruby_sdk)
6
+ [![Gem Version](https://badge.fury.io/rb/spacebunny.svg)](https://badge.fury.io/rb/spacebunny)
7
+
8
+ [SpaceBunny](http://spacebunny.io) is the IoT platform that makes it easy for you and your devices to send and
9
+ exchange messages with a server or even with each other. You can store the data, receive timely event notifications,
10
+ monitor live streams and remotely control your devices. Easy to use, and ready to scale at any time.
11
+
12
+ This is the source code repository for Ruby SDK.
13
+ Please feel free to contribute!
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem 'spacebunny'
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ $ bundle
26
+
27
+ Or install it yourself as:
28
+
29
+ $ gem install spacebunny
30
+
31
+ After you have signed up for a [SpaceBunny](http://spacebunny.io)'s account, follow the
32
+ [Getting Started](http://getting_started_link) guide for a one minute introduction to the platform concepts
33
+ and a super rapid setup.
34
+
35
+ This SDK provides Device and LiveStream clients and currently supports the AMQP protocol.
36
+
37
+ ## Device - Basic usage
38
+
39
+ Pick a device, view its configurations and copy the Device Key. Instantiate a new `Spacebunny::Device` client,
40
+ providing the Device Key:
41
+
42
+ ```ruby
43
+ dev = Spacebunny::Device.new 'device_key'
44
+ ```
45
+
46
+ the SDK will auto-configure, contacting [SpaceBunny APIs](http://doc.spacebunny.io/api) endpoint, retrieving the
47
+ connection configurations and required parameters. Nothing remains but to connect:
48
+
49
+ ```ruby
50
+ dev.connect
51
+ ```
52
+
53
+ ### Publish
54
+
55
+ Ok, all set up! Let's publish some message:
56
+
57
+ ```ruby
58
+ # We're assuming you have created a 'data' channel and you have enabled it for your device
59
+
60
+ # Let's publish, for instance, some JSON. Payload can be everything you want,
61
+ # SpaceBunny does not impose any constraint on format or content.
62
+
63
+ require 'json' # to convert our payload to JSON
64
+
65
+ # Publish one message every second for a minute.
66
+ 60.times do
67
+ # Generate some random data
68
+ payload = { greetings: 'Hello, World!', temp: rand(20.0..25.0), foo: rand(100..200) }.to_json
69
+
70
+ # Publish
71
+ dev.publish :data, payload
72
+
73
+ # Give feedback on what has been published
74
+ puts "Published: #{payload}"
75
+
76
+ # Take a nap...
77
+ sleep 1
78
+ end
79
+ ```
80
+
81
+ Let's check out that our data is really being sent by going to our web dashboard: navigate to devices, select the
82
+ device and click on 'LIVE DATA'. Select the 'data' channel from the dropdown and click **Start**.
83
+ Having published data as JSON allows SpaceBunny's web UI to parse them and visualize a nice
84
+ realtime graph: On the **Chart** tab write `temp` in the input field and press enter.
85
+ You'll see the graph of the `temp` parameter being rendered. If you want to plot more parameters,
86
+ just use a comma as separator e.g: temp, pressure, voltage
87
+ On the **Messages** tab you'll see raw messages' payloads received on this channel.
88
+
89
+ ### Inbox
90
+
91
+ Waiting for and reading messages from the device's Inbox is trivial:
92
+
93
+ ```ruby
94
+ dev.inbox(wait: true, ack: :auto) do |message|
95
+ puts "Received: #{message.payload}"
96
+ end
97
+ ```
98
+
99
+ `wait` option (default false) causes the script to wait forever on the receive block
100
+
101
+ `ack` option can have two values: `:manual` (default) or `:auto`. When `:manual` you are responsible to ack the messages,
102
+ for instance:
103
+
104
+ ```ruby
105
+ dev.inbox(wait: true, ack: :manual) do |message|
106
+ puts "Received: #{message.payload}"
107
+ # Manually ack the message
108
+ message.ack
109
+ end
110
+ ```
111
+ This permits to handle errors or other critical situations
112
+
113
+ ## Live Stream - Basic usage
114
+
115
+ For accessing a Live Stream a Live Stream Key's is required. On SpaceBunny's Web UI, go to the Streams section,
116
+ click on "Live Stream Keys" and pick or create one.
117
+
118
+ ```ruby
119
+ live = Spacebunny::LiveStream.new client: 'live_stream_key_client', secret: 'live_stream_key_secret'
120
+ ```
121
+
122
+ Similarly to the Device client, the SDK will auto-configure itself, contacting [SpaceBunny APIs](http://doc.spacebunny.io/api)
123
+ endpoint, retrieving the connection configurations and required parameters. Nothing remains but to connect:
124
+
125
+ ```ruby
126
+ live.connect
127
+ ```
128
+
129
+ ### Reading live messages
130
+
131
+ Each LiveStream has its own cache that will keep always last 100 messages (FIFO, when there are more than 100 messages,
132
+ the oldest ones get discarded). If you want to consume messages in a parallel way, you shoul use the cache and connect
133
+ as many LiveStream clients as you need: this way messages will be equally distributed to clients.
134
+
135
+ ```ruby
136
+ live.message_from_cache :some_live_stream, wait: true, ack: :auto do |message|
137
+ puts "Received from cache: #{message.payload}"
138
+ end
139
+
140
+ # An equivalent method is:
141
+ # live.message_from :some_live_stream, from_cache: true, wait: true, ack: :auto do |message|
142
+ # puts "Received from cache: #{message.payload}"
143
+ # end
144
+ ```
145
+
146
+ Conversely, if you want that each client will receive a copy of each message, don't use the cache:
147
+
148
+ ```ruby
149
+ live.message_from :some_live_stream, wait: true, ack: :auto do |message|
150
+ puts "Received a copy of: #{message.payload}"
151
+ end
152
+ ```
153
+
154
+ Every client subscribed to the LiveStream in this way will receive a copy of the message.
155
+
156
+ ## TLS
157
+
158
+ Instantiating a TLS-secured connection is trivial:
159
+
160
+ ```ruby
161
+ # For a Device
162
+
163
+ dev = Spacebunny::Device.new key, tls: true
164
+
165
+ # Similarly, for a Live Stream
166
+
167
+ live = Spacebunny::LiveStream.new client, secret, tls: true
168
+ ```
169
+
170
+ ## More examples and options
171
+
172
+ Take a look at the ```examples``` directory for more code samples and further details about available options.
173
+
174
+
175
+ ### Contributing
176
+
177
+ Bug reports and pull requests are welcome on GitHub at https://github.com/FancyPixel/spacebunny_ruby.
178
+ This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere
179
+ to the [Contributor Covenant](contributor-covenant.org) code of conduct.
180
+
181
+ ### Development
182
+
183
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests.
184
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
185
+
186
+ To install this gem onto your local machine, run `bundle exec rake install`.
187
+
188
+ ### License
189
+
190
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,15 @@
1
+ require 'zeitwerk'
2
+ require 'logger'
3
+
4
+ loader = Zeitwerk::Loader.for_gem
5
+ core_ext = "#{__dir__}/fancybox2/core_ext/"
6
+ loader.ignore core_ext
7
+ loader.inflector.inflect 'mqtt_log_device' => 'MQTTLogDevice',
8
+ 'json_formatter' => 'JSONFormatter'
9
+ loader.setup
10
+
11
+ require "#{core_ext}/hash"
12
+ require "#{core_ext}/array"
13
+
14
+ module Fancybox2
15
+ end
@@ -0,0 +1,18 @@
1
+ class Array
2
+ # Extracts options from a set of arguments. Removes and returns the last
3
+ # element in the array if it's a hash, otherwise returns a blank hash.
4
+ #
5
+ # def options(*args)
6
+ # args.extract_options!
7
+ # end
8
+ #
9
+ # options(1, 2) # => {}
10
+ # options(1, 2, a: :b) # => {:a=>:b}
11
+ def extract_options
12
+ if last.is_a?(Hash)
13
+ pop
14
+ else
15
+ {}
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,170 @@
1
+ # :nocov:
2
+ class Hash
3
+ # Returns a new hash with all keys converted using the block operation.
4
+ #
5
+ # hash = { name: 'Rob', age: '28' }
6
+ #
7
+ # hash.transform_keys{ |key| key.to_s.upcase }
8
+ # # => {"NAME"=>"Rob", "AGE"=>"28"}
9
+ def transform_keys
10
+ return enum_for(:transform_keys) unless block_given?
11
+ result = self.class.new
12
+ each_key do |key|
13
+ result[yield(key)] = self[key]
14
+ end
15
+ result
16
+ end
17
+
18
+ # Destructively convert all keys using the block operations.
19
+ # Same as transform_keys but modifies +self+.
20
+ def transform_keys!
21
+ return enum_for(:transform_keys!) unless block_given?
22
+ keys.each do |key|
23
+ self[yield(key)] = delete(key)
24
+ end
25
+ self
26
+ end
27
+
28
+ # Returns a new hash with all keys converted to strings.
29
+ #
30
+ # hash = { name: 'Rob', age: '28' }
31
+ #
32
+ # hash.stringify_keys
33
+ # # => {"name"=>"Rob", "age"=>"28"}
34
+ def stringify_keys
35
+ transform_keys { |key| key.to_s }
36
+ end
37
+
38
+ # Destructively convert all keys to strings. Same as
39
+ # +stringify_keys+, but modifies +self+.
40
+ def stringify_keys!
41
+ transform_keys! { |key| key.to_s }
42
+ end
43
+
44
+ # Returns a new hash with all keys converted to symbols, as long as
45
+ # they respond to +to_sym+.
46
+ #
47
+ # hash = { 'name' => 'Rob', 'age' => '28' }
48
+ #
49
+ # hash.symbolize_keys
50
+ # # => {:name=>"Rob", :age=>"28"}
51
+ def symbolize_keys
52
+ transform_keys { |key| key.to_sym rescue key }
53
+ end
54
+
55
+ alias_method :to_options, :symbolize_keys
56
+
57
+ # Destructively convert all keys to symbols, as long as they respond
58
+ # to +to_sym+. Same as +symbolize_keys+, but modifies +self+.
59
+ def symbolize_keys!
60
+ transform_keys! { |key| key.to_sym rescue key }
61
+ end
62
+
63
+ alias_method :to_options!, :symbolize_keys!
64
+
65
+ # Validate all keys in a hash match <tt>*valid_keys</tt>, raising
66
+ # ArgumentError on a mismatch.
67
+ #
68
+ # Note that keys are treated differently than HashWithIndifferentAccess,
69
+ # meaning that string and symbol keys will not match.
70
+ #
71
+ # { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age"
72
+ # { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'"
73
+ # { name: 'Rob', age: '28' }.assert_valid_keys(:name, :age) # => passes, raises nothing
74
+ def assert_valid_keys(*valid_keys)
75
+ valid_keys.flatten!
76
+ each_key do |k|
77
+ unless valid_keys.include?(k)
78
+ raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}")
79
+ end
80
+ end
81
+ end
82
+
83
+ # Returns a new hash with all keys converted by the block operation.
84
+ # This includes the keys from the root hash and from all
85
+ # nested hashes and arrays.
86
+ #
87
+ # hash = { person: { name: 'Rob', age: '28' } }
88
+ #
89
+ # hash.deep_transform_keys{ |key| key.to_s.upcase }
90
+ # # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}}
91
+ def deep_transform_keys(&block)
92
+ _deep_transform_keys_in_object(self, &block)
93
+ end
94
+
95
+ # Destructively convert all keys by using the block operation.
96
+ # This includes the keys from the root hash and from all
97
+ # nested hashes and arrays.
98
+ def deep_transform_keys!(&block)
99
+ _deep_transform_keys_in_object!(self, &block)
100
+ end
101
+
102
+ # Returns a new hash with all keys converted to strings.
103
+ # This includes the keys from the root hash and from all
104
+ # nested hashes and arrays.
105
+ #
106
+ # hash = { person: { name: 'Rob', age: '28' } }
107
+ #
108
+ # hash.deep_stringify_keys
109
+ # # => {"person"=>{"name"=>"Rob", "age"=>"28"}}
110
+ def deep_stringify_keys
111
+ deep_transform_keys { |key| key.to_s }
112
+ end
113
+
114
+ # Destructively convert all keys to strings.
115
+ # This includes the keys from the root hash and from all
116
+ # nested hashes and arrays.
117
+ def deep_stringify_keys!
118
+ deep_transform_keys! { |key| key.to_s }
119
+ end
120
+
121
+ # Returns a new hash with all keys converted to symbols, as long as
122
+ # they respond to +to_sym+. This includes the keys from the root hash
123
+ # and from all nested hashes and arrays.
124
+ #
125
+ # hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
126
+ #
127
+ # hash.deep_symbolize_keys
128
+ # # => {:person=>{:name=>"Rob", :age=>"28"}}
129
+ def deep_symbolize_keys
130
+ deep_transform_keys { |key| key.to_sym rescue key }
131
+ end
132
+
133
+ # Destructively convert all keys to symbols, as long as they respond
134
+ # to +to_sym+. This includes the keys from the root hash and from all
135
+ # nested hashes and arrays.
136
+ def deep_symbolize_keys!
137
+ deep_transform_keys! { |key| key.to_sym rescue key }
138
+ end
139
+
140
+ private
141
+
142
+ # support methods for deep transforming nested hashes and arrays
143
+ def _deep_transform_keys_in_object(object, &block)
144
+ case object
145
+ when Hash
146
+ object.each_with_object({}) do |(key, value), result|
147
+ result[yield(key)] = _deep_transform_keys_in_object(value, &block)
148
+ end
149
+ when Array
150
+ object.map { |e| _deep_transform_keys_in_object(e, &block) }
151
+ else
152
+ object
153
+ end
154
+ end
155
+
156
+ def _deep_transform_keys_in_object!(object, &block)
157
+ case object
158
+ when Hash
159
+ object.keys.each do |key|
160
+ value = object.delete(key)
161
+ object[yield(key)] = _deep_transform_keys_in_object!(value, &block)
162
+ end
163
+ object
164
+ when Array
165
+ object.map! { |e| _deep_transform_keys_in_object!(e, &block) }
166
+ else
167
+ object
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,19 @@
1
+ require 'logger'
2
+ require 'json'
3
+
4
+ module Fancybox2
5
+ module Logger
6
+ class JSONFormatter < ::Logger::Formatter
7
+ def call(severity, time, progname, msg)
8
+ json = JSON.generate(
9
+ level: severity,
10
+ timestamp: time.utc.strftime('%Y-%m-%dT%H:%M:%S.%3NZ'.freeze),
11
+ #progname: progname,
12
+ message: msg,
13
+ pid: Process.pid
14
+ )
15
+ "#{json}\n"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,28 @@
1
+ module Fancybox2
2
+ module Logger
3
+ class MQTTLogDevice
4
+
5
+ attr_accessor :client, :topic
6
+
7
+ def initialize(topic, *args)
8
+ @topic = topic
9
+ options = args.extract_options.deep_symbolize_keys
10
+ @client = options[:client]
11
+ unless @client.respond_to?(:publish)
12
+ raise ArgumentError, "provided client does not respond to 'publish'"
13
+ end
14
+ end
15
+
16
+ def write(message)
17
+ if @client.connected?
18
+ @client.publish @topic, message
19
+ end
20
+ end
21
+
22
+ def close(*args)
23
+ # Do nothing.
24
+ # Future: close only if client is internal
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,127 @@
1
+ module Fancybox2
2
+ module Logger
3
+
4
+ # Log on multiple loggers at the same time
5
+ #
6
+ # Usage example:
7
+ #
8
+ # file_logger = Logger.new(File.open("log/debug.log", "a"))
9
+ # stdout_logger = Logger.new(STDOUT)
10
+ # Create a logger that logs both to STDOUT and log_file at the same time with 'info' loglevel
11
+ # multi_logger = Fancybox2::Logger::Multi.new(file_logger, stdout_logger, level: :info))
12
+
13
+ class Multi
14
+ attr_accessor :loggers, :level, :escape_data, :progname
15
+
16
+ # logger_1, logger_2, ... , level: nil, loggers: nil, escape_data: true)
17
+ def initialize(*args)
18
+ options = args.extract_options.deep_symbolize_keys
19
+ loggers = args
20
+ if !loggers.is_a?(Array) || loggers.size.zero?
21
+ raise ArgumentError.new("provide at least one logger instance")
22
+ end
23
+
24
+ @level = normalize_log_level(options[:level])
25
+ @escape_data = options[:escape_data] || false
26
+ @progname = options[:progname]
27
+
28
+ self.loggers = loggers
29
+ # Set properties
30
+ # Override Loggers levels only if explicitly required
31
+ self.level = @level if options[:level] # Do not use @level because it has already been processed
32
+ # Override Logger's Formatter only if explicitly required
33
+ self.escape_data = @escape_data if @escape_data
34
+ self.progname = @progname if @progname
35
+
36
+ define_methods
37
+ end
38
+
39
+ def add(level, *args)
40
+ @loggers.each { |logger| logger.add(level, *args) }
41
+ end
42
+ alias log add
43
+
44
+ def add_logger(logger)
45
+ @loggers << logger
46
+ end
47
+
48
+ def close
49
+ @loggers.map(&:close)
50
+ end
51
+
52
+ def default_log_level
53
+ 'info'
54
+ end
55
+
56
+ def escape_data=(value)
57
+ if value
58
+ @loggers.each do |logger|
59
+ escape_data_of logger
60
+ end
61
+ else
62
+ @loggers.each { |logger| logger.formatter = ::Logger::Formatter.new }
63
+ end
64
+ end
65
+
66
+ def level=(level)
67
+ @level = normalize_log_level(level)
68
+ @loggers.each { |logger| logger.level = level }
69
+ end
70
+
71
+ def loggers=(new_loggers)
72
+ @loggers = []
73
+ new_loggers.each do |logger|
74
+ # Check if provided loggers are real Loggers
75
+ unless logger.is_a? ::Logger
76
+ raise ArgumentError.new("one of the provided loggers is not of class Logger, but of class '#{logger.class}'")
77
+ end
78
+ # Add Logger to the list
79
+ add_logger logger
80
+ end
81
+ end
82
+
83
+ def progname=(name)
84
+ loggers.each do |logger|
85
+ logger.progname = name
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def define_methods
92
+ ::Logger::Severity.constants.each do |level|
93
+ define_singleton_method(level.downcase) do |args|
94
+ @loggers.each { |logger| logger.add(normalize_log_level(level.downcase), *args) }
95
+ end
96
+
97
+ define_singleton_method("#{ level.downcase }?".to_sym) do
98
+ @level <= ::Logger::Severity.const_get(level)
99
+ end
100
+ end
101
+ end
102
+
103
+ def escape_data_of(logger)
104
+ original_formatter = ::Logger::Formatter.new
105
+ logger.formatter = proc do |severity, datetime, progname, msg|
106
+ original_formatter.call(severity, datetime, progname, msg.dump)
107
+ end
108
+ end
109
+
110
+ ##
111
+ # @param [String] log_level
112
+ def normalize_log_level(log_level)
113
+ case log_level
114
+ when :unknown, ::Logger::UNKNOWN, 'unknown' then ::Logger::UNKNOWN
115
+ when :debug, ::Logger::DEBUG, 'debug' then ::Logger::DEBUG
116
+ when :info, ::Logger::INFO, 'info' then ::Logger::INFO
117
+ when :warn, ::Logger::WARN, 'warn' then ::Logger::WARN
118
+ when :error, ::Logger::ERROR, 'error' then ::Logger::ERROR
119
+ when :fatal, ::Logger::FATAL, 'fatal' then ::Logger::FATAL
120
+ else
121
+ # puts "Fancybox2::Logger::Multi#normalize_log_level, log_level value '#{log_level.inspect}' not supported, defaulting to '#{default_log_level}'"
122
+ normalize_log_level(default_log_level)
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,426 @@
1
+ require 'json'
2
+ require 'yaml'
3
+ require 'logger'
4
+ require 'paho-mqtt'
5
+ require 'concurrent-ruby'
6
+
7
+ module Fancybox2
8
+ module Module
9
+ class Base
10
+
11
+ attr_reader :logger, :mqtt_client, :fbxfile, :fbxfile_path, :status
12
+ attr_accessor :configs
13
+
14
+ def initialize(fbxfile_path, options = {})
15
+ unless fbxfile_path || fbxfile_path.is_a?(String) || fbxfile_path.empty?
16
+ raise FbxfileNotProvided
17
+ end
18
+
19
+ @fbxfile_path = fbxfile_path
20
+ options.deep_symbolize_keys!
21
+ @internal_mqtt_client = false
22
+
23
+ @fbxfile = check_and_return_fbxfile options.fetch(:fbxfile, load_fbx_file)
24
+ @mqtt_client_params = options[:mqtt_client_params] || {}
25
+ check_or_build_mqtt_client options[:mqtt_client]
26
+ @log_level = options.fetch :log_level, ::Logger::INFO
27
+ @log_progname = options.fetch :log_progname, 'Fancybox2::Module::Base'
28
+ @logger = options.fetch :logger, create_default_logger
29
+ @status = :stopped
30
+ @alive_task = nil
31
+ end
32
+
33
+ def alive_message_data(&block)
34
+ if block_given?
35
+ @alive_message_data = block
36
+ return
37
+ end
38
+ @alive_message_data.call if @alive_message_data
39
+ end
40
+
41
+ def alive_message_data=(callback)
42
+ @alive_message_data = callback if callback.is_a?(Proc)
43
+ end
44
+
45
+ def message_to(dest, action = '', payload = '', retain = false, qos = 2)
46
+ if mqtt_client.connected?
47
+ topic = topic_for dest: dest, action: action
48
+ payload = case payload
49
+ when Hash, Array
50
+ payload.to_json
51
+ else
52
+ payload
53
+ end
54
+ logger.debug "#{self.class}#message_to '#{topic}' payload: #{payload}"
55
+ mqtt_client.publish topic, payload, retain, qos
56
+ else
57
+ logger.error 'MQTT client not connected to broker'
58
+ end
59
+ end
60
+
61
+ def name
62
+ fbxfile[:name]
63
+ end
64
+
65
+ def on_action(action, callback = nil, &block)
66
+ topic = topic_for source: :core, action: action
67
+ mqtt_client.add_topic_callback topic do |packet|
68
+ # :nocov:
69
+ payload = packet.payload
70
+ # Try to parse payload as JSON. Rescue with original payload in case of error
71
+ packet.payload = JSON.parse(payload) rescue payload
72
+ if block_given?
73
+ block.call packet
74
+ elsif callback && callback.is_a?(Proc)
75
+ callback.call packet
76
+ end
77
+ # :nocov:
78
+ end
79
+ end
80
+
81
+ def on_configs(packet = nil, &block)
82
+ logger.debug 'on_configs'
83
+ if block_given?
84
+ @on_configs = block
85
+ return
86
+ end
87
+ @configs = begin
88
+ # Try to parse
89
+ JSON.parse packet.payload
90
+ rescue JSON::ParserError
91
+ logger.debug 'on_configs: failed parsing packet as JSON, retrying with YAML'
92
+ begin
93
+ # Try to parse YAML
94
+ YAML.load packet.payload
95
+ rescue StandardError
96
+ logger.debug 'on_configs: failed parsing packet as YAML. Falling back to raw payload'
97
+ # Fallback to original content
98
+ packet.payload
99
+ end
100
+ end
101
+ @on_configs.call(packet) if @on_configs
102
+ end
103
+
104
+ def on_configs=(callback)
105
+ @on_configs = callback if callback.is_a?(Proc)
106
+ end
107
+
108
+ def on_logger(packet = nil, &block)
109
+ if block_given?
110
+ @on_logger = block
111
+ return
112
+ end
113
+ @on_logger.call(packet) if @on_logger
114
+ logger_configs = packet.payload
115
+ logger.level = logger_configs['level'] if logger_configs['level']
116
+ end
117
+
118
+ def on_logger=(callback)
119
+ @on_logger = callback if callback.is_a?(Proc)
120
+ end
121
+
122
+ def on_restart(packet = nil, &block)
123
+ if block_given?
124
+ @on_restart = block
125
+ return
126
+ end
127
+ @on_restart.call(packet) if @on_restart
128
+ # Stop + start
129
+ on_stop
130
+ on_start packet
131
+ end
132
+
133
+ def on_restart=(callback)
134
+ @on_restart = callback if callback.is_a?(Proc)
135
+ end
136
+
137
+ def on_shutdown(do_exit = true, &block)
138
+ if block_given?
139
+ @on_shutdown = block
140
+ return
141
+ end
142
+
143
+ shutdown_ok = true
144
+ logger.debug "Received 'shutdown' command"
145
+ # Stop sending alive messages
146
+ @alive_task.shutdown if @alive_task
147
+
148
+ begin
149
+ # Call user code if any
150
+ @on_shutdown.call if @on_shutdown
151
+ rescue StandardError => e
152
+ logger.error "Error during shutdown: #{e.message}"
153
+ shutdown_ok = false
154
+ end
155
+
156
+ # Signal core that we've executed shutdown operations.
157
+ # This message is not mandatory, so keep it simple
158
+ shutdown_message = shutdown_ok ? 'ok' : 'nok'
159
+ logger.debug "Sending shutdown message to core with status '#{shutdown_message}'"
160
+ message_to :core, :shutdown, { status: shutdown_message }
161
+ sleep 0.05 # Wait some time in order to be sure that the message has been published (message is not mandatory)
162
+
163
+ if mqtt_client && mqtt_client.connected?
164
+ # Gracefully disconnect from broker and exit
165
+ logger.debug 'Disconnecting from broker'
166
+ mqtt_client.disconnect
167
+ end
168
+
169
+ if do_exit
170
+ # Exit from process
171
+ status_code = shutdown_ok ? 0 : 1
172
+ logger.debug "Exiting with status code #{status_code}"
173
+ exit status_code
174
+ end
175
+ end
176
+
177
+ def on_shutdown=(callback)
178
+ @on_shutdown = callback if callback.is_a?(Proc)
179
+ end
180
+
181
+ def on_start(packet = nil, &block)
182
+ if block_given?
183
+ @on_start = block
184
+ return
185
+ end
186
+ # Call user code
187
+ @on_start.call(packet) if @on_start
188
+
189
+ configs = packet ? packet.payload : {}
190
+ interval = configs['aliveTimeout'] || 1000
191
+ # Start code execution from scratch
192
+ logger.debug "Received 'start'"
193
+ @status = :running
194
+ start_sending_alive interval: interval
195
+ end
196
+
197
+ def on_start=(callback)
198
+ @on_start = callback if callback.is_a?(Proc)
199
+ end
200
+
201
+ def on_stop(&block)
202
+ if block_given?
203
+ @on_stop = block
204
+ return
205
+ end
206
+ @on_stop.call if @on_stop
207
+ @status = :stopped
208
+ # Stop code execution, but keep broker connection and continue to send alive
209
+ end
210
+
211
+ def on_stop=(callback)
212
+ @on_stop = callback if callback.is_a?(Proc)
213
+ end
214
+
215
+ def remove_action(action)
216
+ topic = topic_for source: :core, action: action
217
+ mqtt_client.remove_topic_callback topic
218
+ end
219
+
220
+ def shutdown(do_exit = true)
221
+ on_shutdown do_exit
222
+ end
223
+
224
+ def start
225
+ on_start
226
+ end
227
+
228
+ def start_sending_alive(interval: 5000)
229
+ # TODO: replace the alive interval task with Eventmachine?
230
+ # Interval is expected to be msec, so convert it to secs
231
+ interval /= 1000
232
+ @alive_task.shutdown if @alive_task
233
+ @alive_task = Concurrent::TimerTask.new(execution_interval: interval, timeout_interval: 2, run_now: true) do
234
+ packet = { status: @status, lastSeen: Time.now.utc }
235
+ if @alive_message_data
236
+ packet[:data] = @alive_message_data.call
237
+ end
238
+ message_to :core, :alive, packet
239
+ end
240
+ @alive_task.execute
241
+ end
242
+
243
+ def running?
244
+ @status.eql? :running
245
+ end
246
+
247
+ def stopped?
248
+ @status.eql? :stopped
249
+ end
250
+
251
+ def setup(retry_connection = true)
252
+ unless @setted_up
253
+ begin
254
+ logger.debug 'Connecting to the broker...'
255
+ mqtt_client.connect
256
+ rescue PahoMqtt::Exception => e
257
+ # :nocov:
258
+ logger.error "Error while connecting to the broker: #{e.message}"
259
+ retry if retry_connection
260
+ # :nocov:
261
+ end
262
+
263
+ @setted_up = true
264
+ end
265
+ end
266
+
267
+ def topic_for(source: self.name, dest: self.name, action: nil, packet_type: :msg)
268
+ source = source.to_s
269
+ packet_type = packet_type.to_s
270
+ dest = dest.to_s
271
+ action = action.to_s
272
+
273
+ Config::DEFAULT_TOPIC_FORMAT % [source, packet_type, dest, action]
274
+ end
275
+
276
+ ## MQTT Client callbacks
277
+
278
+ def on_client_connack
279
+ logger.debug 'Connected to the broker'
280
+ # Setup default callbacks
281
+ default_actions.each do |action_name, callback|
282
+ action_name = action_name.to_s
283
+
284
+ on_action action_name do |packet|
285
+ # :nocov:
286
+ if callback.is_a? Proc
287
+ callback.call packet
288
+ else
289
+ logger.warn "No valid callback defined for '#{action_name}'"
290
+ end
291
+ # :nocov:
292
+ end
293
+ end
294
+
295
+ if mqtt_client.subscribed_topics.size.zero?
296
+ # Subscribe to all messages directed to me
297
+ logger.debug 'Making broker subscriptions'
298
+ mqtt_client.subscribe [topic_for(source: '+', action: '+'), 2]
299
+ end
300
+ end
301
+
302
+ # @note Call super if you override this method
303
+ def on_client_suback
304
+ # Client subscribed, we're ready to rock -> Tell core
305
+ logger.debug 'Subscriptions done'
306
+ logger.debug "Sending 'ready' to core"
307
+ message_to :core, :ready
308
+ end
309
+
310
+ # @note Call super if you override this method
311
+ def on_client_unsuback
312
+ end
313
+
314
+ # @note Call super if you override this method
315
+ def on_client_puback(message)
316
+ end
317
+
318
+ # @note Call super if you override this method
319
+ def on_client_pubrel(message)
320
+ end
321
+
322
+ # @note Call super if you override this method
323
+ def on_client_pubrec(message)
324
+ end
325
+
326
+ # @note Call super if you override this method
327
+ def on_client_pubcomp(message)
328
+ end
329
+
330
+ # @note Call super if you override this method
331
+ def on_client_message(message)
332
+ end
333
+
334
+ private
335
+
336
+ def check_or_build_mqtt_client(mqtt_client = nil)
337
+ if mqtt_client
338
+ unless mqtt_client.is_a? PahoMqtt::Client
339
+ raise Exceptions::NotValidMQTTClient.new
340
+ end
341
+ @internal_mqtt_client = false
342
+ @mqtt_client = mqtt_client
343
+ else
344
+ @internal_mqtt_client = true
345
+ @mqtt_client = PahoMqtt::Client.new mqtt_params
346
+ end
347
+ end
348
+
349
+ def check_and_return_fbxfile(hash_attributes)
350
+ raise ArgumentError, 'You must provide an Hash as argument' unless hash_attributes.is_a?(Hash)
351
+ hash_attributes.deep_symbolize_keys
352
+ end
353
+
354
+ def create_default_logger
355
+ stdout_logger = ::Logger.new STDOUT
356
+ broker_logger = ::Logger.new(Logger::MQTTLogDevice.new(topic_for(dest: :core, action: :logs),
357
+ client: mqtt_client),
358
+ formatter: Logger::JSONFormatter.new)
359
+ logger = Logger::Multi.new stdout_logger, broker_logger,
360
+ level: @log_level,
361
+ progname: @log_progname
362
+ logger
363
+ end
364
+
365
+ # :nocov:
366
+ def default_actions
367
+ {
368
+ start: proc { |packet| on_start packet },
369
+ stop: proc { on_stop },
370
+ restart: proc { |packet| on_restart packet },
371
+ shutdown: proc { on_shutdown },
372
+ logger: proc { |packet| on_logger packet },
373
+ configs: proc { |packet| on_configs packet }
374
+ }
375
+ end
376
+ # :nocov:
377
+
378
+ def load_fbx_file
379
+ if File.exists? @fbxfile_path
380
+ @fbxfile = YAML.load(File.read(@fbxfile_path)).deep_symbolize_keys
381
+ else
382
+ raise Exceptions::FbxfileNotFound.new @fbxfile_path
383
+ end
384
+ end
385
+
386
+ # :nocov:
387
+ def mqtt_default_params
388
+ {
389
+ host: 'localhost',
390
+ port: 1883,
391
+ mqtt_version: '3.1.1',
392
+ clean_session: true,
393
+ persistent: true,
394
+ blocking: false,
395
+ reconnect_limit: -1,
396
+ reconnect_delay: 1,
397
+ client_id: nil,
398
+ username: nil,
399
+ password: nil,
400
+ ssl: false,
401
+ will_topic: nil,
402
+ will_payload: nil,
403
+ will_qos: 0,
404
+ will_retain: false,
405
+ keep_alive: 7,
406
+ ack_timeout: 5,
407
+ on_connack: proc { on_client_connack },
408
+ on_suback: proc { on_client_suback },
409
+ on_unsuback: proc { on_client_unsuback },
410
+ on_puback: proc { |msg| on_client_puback msg },
411
+ on_pubrel: proc { |msg| on_client_pubrel msg },
412
+ on_pubrec: proc { |msg| on_client_pubrec msg },
413
+ on_pubcomp: proc { |msg| on_client_pubcomp msg },
414
+ on_message: proc { |msg| on_client_message msg }
415
+ }
416
+ end
417
+ # :nocov:
418
+
419
+ def mqtt_params
420
+ return @mqtt_params if @mqtt_params
421
+ @mqtt_params = mqtt_default_params.merge(@mqtt_client_params) { |key, old_val, new_val| new_val.nil? ? old_val : new_val }
422
+ @mqtt_params
423
+ end
424
+ end
425
+ end
426
+ end
@@ -0,0 +1,7 @@
1
+ module Fancybox2
2
+ module Module
3
+ class Config
4
+ DEFAULT_TOPIC_FORMAT = '%s/%s/%s/%s'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,27 @@
1
+ module Fancybox2
2
+ module Module
3
+ module Exceptions
4
+
5
+ class FbxfileNotProvided < StandardError
6
+ def initialize(file_path, message = nil)
7
+ message = message || "Fbxfile.example path not provided. Given: #{file_path}"
8
+ super(message)
9
+ end
10
+ end
11
+
12
+ class FbxfileNotFound < StandardError
13
+ def initialize(file_path, message = nil)
14
+ message = message || "Fbxfile.example not found at #{file_path}"
15
+ super(message)
16
+ end
17
+ end
18
+
19
+ class NotValidMQTTClient < StandardError
20
+ def initialize(message = nil)
21
+ message = message || 'The provided MQTT client is not an instance of PahoMqtt::Client'
22
+ super(message)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,38 @@
1
+ module Fancybox2
2
+ module Utils
3
+ module Os
4
+ extend self
5
+
6
+ def identifier
7
+ return @indentifier if @indentifier
8
+
9
+ host_os = RbConfig::CONFIG['host_os']
10
+ case host_os
11
+ when /aix(.+)$/
12
+ 'aix'
13
+ when /darwin(.+)$/
14
+ 'darwin'
15
+ when /linux/
16
+ 'linux'
17
+ when /freebsd(.+)$/
18
+ 'freebsd'
19
+ when /openbsd(.+)$/
20
+ 'openbsd'
21
+ when /netbsd(.*)$/
22
+ 'netbsd'
23
+ when /dragonfly(.*)$/
24
+ 'dragonflybsd'
25
+ when /solaris2/
26
+ 'solaris2'
27
+ when /mswin|mingw32|windows/
28
+ # No Windows platform exists that was not based on the Windows_NT kernel,
29
+ # so 'windows' refers to all platforms built upon the Windows_NT kernel and
30
+ # have access to win32 or win64 subsystems.
31
+ 'windows'
32
+ else
33
+ host_os
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,3 @@
1
+ module Fancybox2
2
+ VERSION = '0.0.1'
3
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fancybox2
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alessandro Verlato
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-10-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: zeitwerk
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 2.3.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 2.3.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: concurrent-ruby
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.1.6
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.1.6
41
+ description:
42
+ email: alessandro@fancypixel.it
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - MIT-LICENSE
48
+ - README.md
49
+ - lib/fancybox2.rb
50
+ - lib/fancybox2/core_ext/array.rb
51
+ - lib/fancybox2/core_ext/hash.rb
52
+ - lib/fancybox2/logger/json_formatter.rb
53
+ - lib/fancybox2/logger/mqtt_log_device.rb
54
+ - lib/fancybox2/logger/multi.rb
55
+ - lib/fancybox2/module/base.rb
56
+ - lib/fancybox2/module/config.rb
57
+ - lib/fancybox2/module/exceptions.rb
58
+ - lib/fancybox2/utils/os.rb
59
+ - lib/fancybox2/version.rb
60
+ homepage: https://github.com/Fancybox2/ruby-sdk
61
+ licenses:
62
+ - MIT
63
+ metadata: {}
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 2.5.0
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubygems_version: 3.1.4
80
+ signing_key:
81
+ specification_version: 4
82
+ summary: Fancybox 2 Ruby SDK
83
+ test_files: []