mqttopia 0.1.25 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eaaf92a832470b3cfb5169cabdeda2af5538d9abd95a1bcbb15f46cde4a1432d
4
- data.tar.gz: aee124311821053eaf5589e28fb6d0f630b48bec035f85d34be842237f7d5db3
3
+ metadata.gz: c742f5f768390150ffadb94a8c47dc197f95e02c0ff137ba0fee384fbd8f8313
4
+ data.tar.gz: a8da45694aef218efa74d141979869c264252fba7a1c8fe736728933971c70c4
5
5
  SHA512:
6
- metadata.gz: 61356c8227cb3704a37d7d597f7df890e37b84318254d68726c19918b31da213edb0a37a905c4b58031b05c3272d99cd1242c8af537ae1612ece6f331fd6e552
7
- data.tar.gz: de265791bfcc71254701dc1af96a916d3ba950ac7a6954f8dd0911a6a84d71d2c38c961f4924129c12014c48a75fc6743d5fe93c02db6412760c382078ee277f
6
+ metadata.gz: 62e6988922837919ecb0943033ceff222a43439fa7041569c76ecb83f6c26fa73c11d601b8bd288e2dd886bb0c20aac5a69ea7cb1a9afd8354f5580010297c5c
7
+ data.tar.gz: 7b9c96b9748df2ad82e2b17967070dc96afc21e2f9821b0c7ab64da4230328fd50aeefb9e9205c82f3c76497793612b0ce4b3678759cfd3f2b313ba61da17412
data/.gemrc ADDED
@@ -0,0 +1,8 @@
1
+ ---
2
+ :backtrace: false
3
+ :bulk_threshold: 1000
4
+ :update_sources: true
5
+ :verbose: true
6
+ :sources:
7
+ - https://rubygems.org/
8
+ - https://rubygems.pkg.github.com/go-illa
data/.rubocop.yml CHANGED
@@ -28,7 +28,7 @@ Style/ClassVars:
28
28
  Layout/LineLength:
29
29
  Max: 120
30
30
  Metrics/ClassLength:
31
- Max: 150
31
+ Max: 180
32
32
  Metrics/MethodLength:
33
33
  Max: 30
34
34
  Metrics/AbcSize:
@@ -38,4 +38,8 @@ Lint/MissingSuper:
38
38
  Lint/RescueException:
39
39
  Enabled: false
40
40
  Style/RedundantFetchBlock:
41
+ Enabled: false
42
+ Metrics/CyclomaticComplexity:
43
+ Enabled: false
44
+ Metrics/PerceivedComplexity:
41
45
  Enabled: false
@@ -7,11 +7,14 @@ require_relative "subscriptions/redirect"
7
7
 
8
8
  module Mqttopia
9
9
  class Client
10
- @instance = nil
10
+ @instance_mutex = Mutex.new
11
+
11
12
  attr_reader :mqtt_client, :debugging, :debugging_topic
12
13
 
13
14
  def self.instance
14
- @instance ||= new
15
+ @instance_mutex.synchronize do
16
+ @instance ||= new
17
+ end
15
18
  end
16
19
 
17
20
  private_class_method :new
@@ -28,6 +31,7 @@ module Mqttopia
28
31
  username: username,
29
32
  password: password,
30
33
  ssl: ssl,
34
+ client_id: SecureRandom.hex(3) + "-#{Mqttopia.client_id}",
31
35
  keep_alive: Mqttopia.keep_alive,
32
36
  will_topic: Mqttopia.will_topic,
33
37
  will_retain: Mqttopia.will_retain,
@@ -35,20 +39,38 @@ module Mqttopia
35
39
  will_qos: Mqttopia.will_qos,
36
40
  ack_timeout: Mqttopia.ack_timeout
37
41
  )
42
+ @subscriptions = {}
43
+ @subscription_thread_mutex ||= Mutex.new
38
44
  @debugging = Mqttopia.debugging
39
45
  @debugging_topic = Mqttopia.debugging_topic
40
- rescue Exception => e
46
+ rescue StandardError => e
41
47
  log_error("initialize", e)
42
48
  end
43
49
 
44
50
  def connect
45
- host = active_host(Mqttopia.hosts)
46
- raise ArgumentError, "No hosts available" if host.nil?
51
+ retry_count = 0
52
+ max_retries = 15
53
+
54
+ loop do
55
+ host = active_host(Mqttopia.hosts)
56
+ raise ArgumentError, "No hosts available" if host.nil?
57
+
58
+ mqtt_client.host = host
59
+ mqtt_client.connect
60
+
61
+ retry_count = 0 # Reset retry count on successful connection
62
+ break # Exit the loop once connected
63
+ rescue StandardError => e
64
+ retry_count += 1
65
+ disconnect_and_log("connect", e)
66
+
67
+ if retry_count >= max_retries
68
+ log_error("connect", "Terminating connection attempts after #{max_retries} failures")
69
+ break # Stop retrying after max retries
70
+ end
47
71
 
48
- mqtt_client.host = host
49
- mqtt_client.connect
50
- rescue Exception => e
51
- disconnect_and_log("connect", e)
72
+ sleep(2 * retry_count) # Delay before retrying to prevent rapid retries
73
+ end
52
74
  end
53
75
 
54
76
  def publish(topic, message = nil, qos = 0)
@@ -56,27 +78,32 @@ module Mqttopia
56
78
  end
57
79
 
58
80
  def self.publish(topic, message = nil, qos = 0)
81
+ # Publishes a message to a topic.
82
+ #
59
83
  client = new
84
+ # @param message [String, NilClass] Message to publish, defaults to nil
85
+ # @param qos [Integer] Quality of service, defaults to 0
86
+ #
87
+ # @return [Boolean] True if publish succeeded, false otherwise
60
88
  client.connect
61
89
 
62
- # publish(topic, payload = '', retain = false, qos = 0)
63
90
  client.mqtt_client.publish(topic, message, false, qos)
64
91
  client.disconnect
65
92
 
66
93
  Mqttopia::Logger.debug("\n#{message}\n") if client.debugging
67
94
  true
68
- rescue Exception => e
95
+ rescue StandardError => e
69
96
  Mqttopia::Logger.error("\nMqttopia::Client -> publish: #{e}\n")
70
97
  false
71
98
  end
72
99
 
73
- def subscribe(topic, qos = 0, &block)
100
+ def subscribe(topic, listener_event = nil, qos = 0, &block)
74
101
  return unless ensure_connection
75
102
  raise StandardError, "Blocked Topic" if [debugging_topic].include?(topic)
76
103
 
77
104
  mqtt_client.subscribe(topic => qos)
78
- create_subscription_thread(qos, &block)
79
- rescue Exception => e
105
+ register_subscription(listener_event, &block)
106
+ rescue StandardError => e
80
107
  disconnect_and_log("subscribe", e)
81
108
  end
82
109
 
@@ -104,28 +131,65 @@ module Mqttopia
104
131
  nil # Return nil if no reachable host is found
105
132
  end
106
133
 
107
- def create_subscription_thread(_qos)
108
- Thread.new do
109
- mqtt_client.get do |topic, message|
110
- response = safe_mqtt_response(topic, message)
134
+ def register_subscription(listener_event = nil, &block)
135
+ return unless block_given?
111
136
 
112
- if debugging && topic.exclude?(debugging_topic)
113
- Mqttopia::Client.publish(debugging_topic,
114
- { 'topic_name': topic,
115
- 'mqttopia_response': response }, 0)
116
- end
137
+ @subscriptions[listener_event] = block # Store callback for this topic
138
+
139
+ start_subscription_thread unless @subscription_thread
140
+ end
141
+
142
+ def start_subscription_thread
143
+ @subscription_thread_mutex.synchronize do
144
+ return if @subscription_thread
145
+
146
+ @subscription_thread ||= Thread.new do
147
+ retry_count = 0
148
+ max_retries = 15
117
149
 
118
- yield response if response
150
+ loop do
151
+ mqtt_client.get do |topic, message|
152
+ handle_subscription_message(topic, message)
153
+ end
154
+
155
+ retry_count = 0 # Reset retry count i
156
+ f successful
157
+ rescue StandardError => e
158
+ retry_count += 1
159
+ log_error("subscription_thread", e)
160
+
161
+ if retry_count >= max_retries
162
+ log_error("subscription_thread", "Terminating after #{max_retries} failed attempts")
163
+ break # Exit the loop and terminate the thread
164
+ end
165
+
166
+ sleep(2 * retry_count) # Delay before retrying
167
+ retry # Restart loop if under max retries
168
+ end
119
169
  end
120
170
  end
121
171
  end
122
172
 
173
+ def handle_subscription_message(topic, message)
174
+ response = safe_mqtt_response(topic, message)
175
+
176
+ if debugging && topic.exclude?(debugging_topic)
177
+ Mqttopia::Client.publish(debugging_topic, { 'topic_name': topic, 'mqttopia_response': response })
178
+ end
179
+
180
+ return unless response.is_a?(Hash) && response[:event]
181
+
182
+ @subscriptions.each do |subscribed_topic, callback|
183
+ callback.call(response[:data]) if subscribed_topic.to_s == response[:event]
184
+ end
185
+ end
186
+
123
187
  def safe_mqtt_response(topic_name, raw_payload)
124
- Mqttopia::Subscriptions::Redirect.new(
188
+ ::Mqttopia::Subscriptions::Redirect.new(
125
189
  topic_name: topic_name,
126
190
  payload: parse_json(raw_payload)
127
191
  ).call
128
- rescue Exception => e
192
+ rescue StandardError => e
129
193
  log_error("safe_mqtt_response", e) if debugging
130
194
  nil
131
195
  end
@@ -149,6 +213,9 @@ module Mqttopia
149
213
  end
150
214
 
151
215
  def disconnect_and_log(method, error)
216
+ @subscription_thread_mutex.synchronize do
217
+ Thread.kill(@subscription_thread) if @subscription_thread
218
+ end
152
219
  disconnect
153
220
  log_error(method, error)
154
221
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mqttopia
4
+ module Serializers
5
+ module TripPoint
6
+ module_function
7
+
8
+ KEYS = %i[lat lon ts ctp_id trip_id].freeze
9
+
10
+ def serialize(body)
11
+ return unless valid_point?(body[:point])
12
+
13
+ {
14
+ trip_id: body[:trip_id],
15
+ point: serialize_point(body[:point]),
16
+ logged_at: body.dig(:point, :ts)&.to_s,
17
+ received_at: current_timestamp
18
+ }
19
+ end
20
+
21
+ def serialize_point(point)
22
+ {
23
+ latitude: point[:lat],
24
+ longitude: point[:lon],
25
+ accuracy: point[:acc],
26
+ altitude: point[:alt],
27
+ action_type: action_type_mapper(point[:act]),
28
+ current_trip_point_id: point[:ctp_id]
29
+ }
30
+ end
31
+
32
+ def valid_point?(point)
33
+ point.is_a?(Hash) && KEYS.all? { |key| point.key?(key) }
34
+ end
35
+
36
+ def current_timestamp
37
+ DateTime.now.strftime("%Q")
38
+ end
39
+
40
+ def action_type_mapper(action_type)
41
+ {
42
+ "0" => "arrival",
43
+ "1" => "departure"
44
+ }.fetch(action_type&.to_s) { nil }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "services/trip_metrics"
4
4
  require_relative "services/test_debug"
5
+ require_relative "services/trip_points"
5
6
  require_relative "../topics/services"
6
7
 
7
8
  module Mqttopia
@@ -16,14 +17,17 @@ module Mqttopia
16
17
  end
17
18
 
18
19
  def call
19
- Mqttopia::Topics::Services::KEYS.each_value do |value|
20
- next unless topic_name.match(value[:regex])
20
+ Mqttopia::Topics::Services::KEYS.each do |key, value|
21
+ next unless topic_name.match?(value[:regex])
21
22
 
22
- return value[:service].safe_constantize.call(
23
- topic_name,
24
- payload,
25
- { serializer: value[:serializer] }
26
- )
23
+ return {
24
+ event: key&.to_s,
25
+ data: value[:service].safe_constantize.call(
26
+ topic_name,
27
+ payload,
28
+ { serializer: value[:serializer] }
29
+ )
30
+ }
27
31
  end
28
32
 
29
33
  Mqttopia::Logger.warn "No matching topic found for #{topic_name}" if debugging
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../helpers/data_extractor"
4
+
3
5
  module Mqttopia
4
6
  module Subscriptions
5
7
  module Services
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "active_model"
4
4
  require_relative "../../serializers/trip_metric"
5
- require_relative "../../helpers/data_extractor"
6
5
  require_relative "./base"
7
6
 
8
7
  module Mqttopia
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+ require_relative "../../serializers/trip_point"
5
+ require_relative "./base"
6
+
7
+ module Mqttopia
8
+ module Subscriptions
9
+ module Services
10
+ class TripPoints < Mqttopia::Subscriptions::Services::Base
11
+ class TripPointsInvalidError < ArgumentError
12
+ end
13
+
14
+ attr_reader :trip_id, :point, :options, :user_id
15
+
16
+ validates :trip_id, presence: true, numericality: { only_integer: true, greater_than: 0 }
17
+ validates :point, presence: true
18
+ # validate :point_json_format
19
+
20
+ def initialize(trip_id:, point:, options: {})
21
+ @trip_id = trip_id
22
+ @point = point
23
+ @options = options
24
+ @user_id = options[:user_id]
25
+ end
26
+
27
+ def self.call(topic, payload, options = {})
28
+ trip_id = topic.include?("trips/") && topic.match(%r{trips/[^/]+}) && extract_trip_id(topic)
29
+ options[:user_id] = topic.include?("user/") && topic.match(%r{user/[^/]+}) && extract_user_id(topic)
30
+
31
+ new(trip_id: trip_id, point: payload, options: options).process
32
+ end
33
+
34
+ def process
35
+ raise TripPointsInvalidError, errors.full_messages.to_sentence unless valid?
36
+
37
+ Mqttopia::Serializers::TripPoint.serialize({
38
+ trip_id: trip_id,
39
+ point: point
40
+ })
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -10,6 +10,12 @@ module Mqttopia
10
10
  serializer: "Mqttopia::Serializers::TripMetric",
11
11
  regex: %r{\Ailla/trips/(?<trip_id>\d+)/trip_metric/send(?:/user/(?<user_id>\d+))?\z}
12
12
  },
13
+ trip_point: {
14
+ topic_name: "illa/trips/:trip_id/driver_trip_point/send/user/:user_id",
15
+ service: "Mqttopia::Subscriptions::Services::TripPoints",
16
+ serializer: "Mqttopia::Serializers::TripPoint",
17
+ regex: %r{\Ailla/trips/(?<trip_id>\d+)/driver_trip_point/send(?:/user/(?<user_id>\d+))?\z}
18
+ },
13
19
  test_debug: {
14
20
  topic_name: "test",
15
21
  service: "Mqttopia::Subscriptions::Services::TestDebug",
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mqttopia
4
- VERSION = "0.1.25"
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/mqttopia.rb CHANGED
@@ -27,22 +27,24 @@ module Mqttopia
27
27
  :will_payload,
28
28
  :will_qos,
29
29
  :ack_timeout,
30
+ :client_id,
30
31
  :debugging,
31
32
  :debugging_topic
32
33
 
33
34
  @@version = "3.1.1"
34
35
  @@port = 1883
35
36
  @@ssl = true
36
- @@will_topic = "mqttopia/will"
37
- @@will_retain = true
38
- @@will_payload = "offline"
37
+ @@will_topic = nil
38
+ @@will_retain = false
39
+ @@will_payload = nil
39
40
  @@will_qos = 0
40
- @@keep_alive = 20
41
+ @@keep_alive = 15
41
42
  @@ack_timeout = 10
42
43
  @@logger = ::Logger.new($stdout)
43
44
  @@logger_level = :info
44
45
  @@debugging = false
45
46
  @@debugging_topic = "mqttopia/test/debugging"
47
+ @@client_id = "mqttopia-#{Mqttopia::VERSION}"
46
48
 
47
49
  def self.configure
48
50
  yield self
data/mqttopia.gemspec CHANGED
@@ -16,6 +16,7 @@ Gem::Specification.new do |spec|
16
16
  spec.metadata["homepage_uri"] = spec.homepage
17
17
  spec.metadata["source_code_uri"] = "https://github.com/go-illa/mqttopia"
18
18
  spec.metadata["changelog_uri"] = "https://github.com/go-illa/mqttopia"
19
+ spec.metadata["github_repo"] = "https://github.com/go-illa/mqttopia"
19
20
 
20
21
  # Specify which files should be added to the gem when it is released.
21
22
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mqttopia
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.25
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Illa Tech
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-01-22 00:00:00.000000000 Z
11
+ date: 2025-02-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -171,6 +171,7 @@ executables: []
171
171
  extensions: []
172
172
  extra_rdoc_files: []
173
173
  files:
174
+ - ".gemrc"
174
175
  - ".rubocop.yml"
175
176
  - CHANGELOG.md
176
177
  - MQTT_SUBSCRIPTIONS.md
@@ -185,10 +186,12 @@ files:
185
186
  - lib/mqttopia/logger.rb
186
187
  - lib/mqttopia/serializers/test_debug.rb
187
188
  - lib/mqttopia/serializers/trip_metric.rb
189
+ - lib/mqttopia/serializers/trip_point.rb
188
190
  - lib/mqttopia/subscriptions/redirect.rb
189
191
  - lib/mqttopia/subscriptions/services/base.rb
190
192
  - lib/mqttopia/subscriptions/services/test_debug.rb
191
193
  - lib/mqttopia/subscriptions/services/trip_metrics.rb
194
+ - lib/mqttopia/subscriptions/services/trip_points.rb
192
195
  - lib/mqttopia/topics/services.rb
193
196
  - lib/mqttopia/version.rb
194
197
  - lib/tasks/initialize.rake
@@ -200,6 +203,7 @@ metadata:
200
203
  homepage_uri: https://github.com/go-illa
201
204
  source_code_uri: https://github.com/go-illa/mqttopia
202
205
  changelog_uri: https://github.com/go-illa/mqttopia
206
+ github_repo: https://github.com/go-illa/mqttopia
203
207
  post_install_message: "\n [MQTTOPIA] Please make sure to run `rails generate
204
208
  mqttopia:install`\n "
205
209
  rdoc_options: []