quelink-mg 0.6.2 → 0.7.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -0
  3. data/Gemfile.lock +2 -1
  4. data/README.md +218 -7
  5. data/lib/quelink-mg/at/gtupd.rb +3 -2
  6. data/lib/quelink-mg/device_type.rb +13 -0
  7. data/lib/quelink-mg/gl30meu/ack/gtbsi.rb +15 -0
  8. data/lib/quelink-mg/gl30meu/ack/gtcfg.rb +15 -0
  9. data/lib/quelink-mg/gl30meu/ack/gtrto.rb +16 -0
  10. data/lib/quelink-mg/gl30meu/ack/gtsri.rb +15 -0
  11. data/lib/quelink-mg/gl30meu/at/gtbsi.rb +36 -0
  12. data/lib/quelink-mg/gl30meu/at/gtcfg.rb +46 -0
  13. data/lib/quelink-mg/gl30meu/at/gtrto.rb +32 -0
  14. data/lib/quelink-mg/gl30meu/at/gtsri.rb +39 -0
  15. data/lib/quelink-mg/gl30meu/at/gtupd.rb +35 -0
  16. data/lib/quelink-mg/gl30meu/buff/gtfri.rb +18 -0
  17. data/lib/quelink-mg/gl30meu/resp/gtati.rb +18 -0
  18. data/lib/quelink-mg/gl30meu/resp/gtfri.rb +18 -0
  19. data/lib/quelink-mg/gl30meu/resp/gtinf.rb +19 -0
  20. data/lib/quelink-mg/gl30meu/resp/gtupc.rb +16 -0
  21. data/lib/quelink-mg/gl30meu/resp/gtupd.rb +15 -0
  22. data/lib/quelink-mg/resp/base.rb +1 -1
  23. data/lib/quelink_mg.rb +21 -0
  24. data/quelink-mg.gemspec +1 -1
  25. data/spec/quelink_mg/at/gtupd_spec.rb +1 -1
  26. data/spec/quelink_mg/device_type_spec.rb +29 -0
  27. data/spec/quelink_mg/gl30meu/ack/gtcfg_spec.rb +16 -0
  28. data/spec/quelink_mg/gl30meu/ack/gtrto_spec.rb +17 -0
  29. data/spec/quelink_mg/gl30meu/at/gtbsi_spec.rb +47 -0
  30. data/spec/quelink_mg/gl30meu/at/gtcfg_spec.rb +76 -0
  31. data/spec/quelink_mg/gl30meu/at/gtrto_spec.rb +59 -0
  32. data/spec/quelink_mg/gl30meu/buff/gtfri_spec.rb +17 -0
  33. data/spec/quelink_mg/gl30meu/resp/gtati_spec.rb +36 -0
  34. data/spec/quelink_mg/gl30meu/resp/gtfri_spec.rb +36 -0
  35. data/spec/quelink_mg/gl30meu/resp/gtinf_spec.rb +33 -0
  36. metadata +29 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f8b5e9cc7cba93f6d85db7ed831019cabfb02abf090fecc44e01e237f8d2414
4
- data.tar.gz: caa4cf0d144da3a5874ca953e29de208ad5a20076c954024c627568dd82b9d76
3
+ metadata.gz: da1518d252576d756cd72b6f4e4a6a0d3e8a0b6966036d83aad656c15de06480
4
+ data.tar.gz: 3485cf01a3a011efcf5463c8f0104359697cbbe5f6d9834b7d11280a0e25f12b
5
5
  SHA512:
6
- metadata.gz: 027f0b07013d19f9be4302d9697c25348d1e9de222e72abd486b4103c5b79a17324ea8b54b1d7201e08606bbbf4c4850d4a393fc9978e6ff78b876a88944a39e
7
- data.tar.gz: 2d759ab9190dd886d9aca5770af7b17a9c3f1c0e0a23498631b869a7897ffca35687299c5554dab6c6d77d1c774ec5264d080da9a06b04c67063a773a0b75753
6
+ metadata.gz: d9e5aa060f4a3eccbf2898341f7af0b94f845c2ed94b33b020da629be386a81617b4a4b5501fee2791c42cac9ded0d40e60239cea596c7bead7d6076207c6717
7
+ data.tar.gz: 79bc01b361e07608279ed119adefd87fc59da0e38afbd2270dabdaea76c1f557f0eff254a977c9ef5b1c1c3f1dc5efd4d9541cb02a14a721e3a2ba77a4e61378
data/.rubocop.yml CHANGED
@@ -6,6 +6,10 @@ AllCops:
6
6
  TargetRubyVersion: 3.1
7
7
  Style/Documentation:
8
8
  Enabled: false
9
+ Style/EndOfLine:
10
+ Enabled: false
11
+ Style/OptionalBooleanParameter:
12
+ Enabled: false
9
13
  Metrics/MethodLength:
10
14
  Max: 18
11
15
  Metrics/BlockLength:
@@ -19,3 +23,5 @@ RSpec/ExampleLength:
19
23
  Layout/LineLength:
20
24
  Exclude:
21
25
  - spec/quelink_mg/resp/*_spec.rb
26
+ - spec/quelink_mg/gl30meu/**/*_spec.rb
27
+ - spec/quelink_mg/**.rb
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- quelink-mg (0.6.1)
4
+ quelink-mg (0.7.0)
5
5
  activesupport
6
6
 
7
7
  GEM
@@ -72,6 +72,7 @@ GEM
72
72
  unicode-display_width (2.4.2)
73
73
 
74
74
  PLATFORMS
75
+ arm64-darwin-25
75
76
  x86_64-linux
76
77
 
77
78
  DEPENDENCIES
data/README.md CHANGED
@@ -1,26 +1,237 @@
1
1
  # quelink-mg
2
- Management of commands sent or received from QUELINK-300 device serie. Tested with MG-310 and MG-320
3
2
 
4
- ## Instalation
3
+ Management of commands sent or received from Queclink GPS tracker devices. Supports **GL320M** and **GL30MEU** (GL30MEUR01).
4
+
5
+ ## Installation
5
6
 
6
7
  The quelink-mg gem is available at rubygems.org. You can install with:
7
8
 
8
- `gem install quelink-mg`
9
+ ```
10
+ gem install quelink-mg
11
+ ```
9
12
 
10
13
  Alternatively, you can install the gem with bundler:
11
14
 
12
- \# Gemfile
13
-
14
- `gem 'quelink-mg'`
15
+ ```ruby
16
+ # Gemfile
17
+ gem 'quelink-mg'
18
+ ```
15
19
 
16
20
  After doing bundle install, you should have the gem installed in your bundle.
17
21
 
22
+ ## Configuration
23
+
24
+ ```ruby
25
+ QuelinkMg.configure do |config|
26
+ config.time_zone = ActiveSupport::TimeZone.new('Europe/Warsaw') # default
27
+ end
28
+ ```
29
+
30
+ ## Supported devices
31
+
32
+ | Feature | GL320M | GL30MEU |
33
+ |---------|--------|---------|
34
+ | Protocol prefix | `C3` | `97` |
35
+ | Default password | `gl320m` | `gl30` |
36
+ | Namespace | `QuelinkMg::At`, `QuelinkMg::Resp`, etc. | `QuelinkMg::Gl30meu::At`, `QuelinkMg::Gl30meu::Resp`, etc. |
37
+
38
+ ## Usage
39
+
40
+ ### Device type detection
41
+
42
+ Detect the device model from any raw message using the protocol version prefix:
43
+
44
+ ```ruby
45
+ response = 'C30204,860201061504521,,0,0,1,...'
46
+ device_type = QuelinkMg::DeviceType.detect(response.split(',').first)
47
+ # => :gl320m
48
+
49
+ response = '970101,861106059716756,GL30MEU,0,0,1,...'
50
+ device_type = QuelinkMg::DeviceType.detect(response.split(',').first)
51
+ # => :gl30meu
52
+ ```
53
+
54
+ ### Building AT commands
55
+
56
+ AT commands are sent from the server to the device. Build them by passing a params hash:
57
+
58
+ ```ruby
59
+ # GL320M — set report intervals (GTFRI)
60
+ command = QuelinkMg::At::Gtfri.new(params: {
61
+ password: 'gl320m',
62
+ mode: 2,
63
+ check_interval: 30,
64
+ send_interval: 60,
65
+ serial_number: 'FFFF'
66
+ })
67
+ command.message
68
+ # => "AT+GTFRI=gl320m,,,,,,30,60,,,,,,,,,,,,FFFF$"
69
+
70
+ # GL320M — remote control / query firmware version (GTRTO)
71
+ command = QuelinkMg::At::Gtrto.new(params: {
72
+ password: 'gl320m',
73
+ sub_command: 8,
74
+ serial_number: 'FFFF'
75
+ })
76
+ command.message
77
+ # => "AT+GTRTO=gl320m,8,,,,,,FFFF$"
78
+
79
+ # GL30MEU — configure device (GTCFG)
80
+ command = QuelinkMg::Gl30meu::At::Gtcfg.new(params: {
81
+ password: 'gl30',
82
+ continuous_send_interval: 30,
83
+ gnss_enable: 1,
84
+ led_on: 1,
85
+ serial_number: 'FFFF'
86
+ })
87
+ command.message
88
+ # => "AT+GTCFG=gl30,,,,,,,30,,,,,,,,1,,,,,,,,,1,FFFF$"
89
+
90
+ # GL30MEU — set APN (GTBSI)
91
+ command = QuelinkMg::Gl30meu::At::Gtbsi.new(params: {
92
+ password: 'gl30',
93
+ lte_apn: 'iot.1nce.net',
94
+ network_mode: 2,
95
+ lte_mode: 2,
96
+ serial_number: 'FFFF'
97
+ })
98
+ command.message
99
+ # => "AT+GTBSI=gl30,,iot.1nce.net,,,2,2,,,FFFF$"
100
+ ```
101
+
102
+ Invalid parameters raise specific exceptions:
103
+
104
+ ```ruby
105
+ QuelinkMg::At::Gtfri.new(params: {
106
+ password: 'gl320m',
107
+ mode: 99, # invalid — must be 0..5
108
+ serial_number: 'FFFF'
109
+ }).message
110
+ # => raises InvalidATGTFRIException
111
+ ```
112
+
113
+ ### Parsing device responses (RESP)
114
+
115
+ Parse real-time messages sent from device to server. Values are automatically type-cast (integers, floats, timezone-aware timestamps):
116
+
117
+ ```ruby
118
+ # GL320M — position report (GTFRI)
119
+ response = 'C30204,860201061504521,,0,0,1,1,0.0,0,96.2,21.012847,52.200338,20230813061232,0260,0003,E31F,0447020D,,34,20230813061231,3E94'
120
+ parsed = QuelinkMg::Resp::Gtfri.new(response:).hash
121
+ # => {
122
+ # "protocol_version" => "C30204",
123
+ # "unique_id" => 860201061504521,
124
+ # "longitude" => 21.012847,
125
+ # "latitude" => 52.200338,
126
+ # "speed" => 0.0,
127
+ # "battery_percentage" => 34,
128
+ # "gps_utc_time" => 2023-08-13 08:12:32 +0200,
129
+ # ...
130
+ # }
131
+
132
+ # GL30MEU — position report (GTFRI) — different fields
133
+ response = '970101,861106059716756,GL30MEU,0,0,1,1,0.0,70,17.8,121.348554,31.163204,20231011084221,0460,0000,5B63,0867349C,21,0,3552,2,1,0,,20231011084241,1A0C'
134
+ parsed = QuelinkMg::Gl30meu::Resp::Gtfri.new(response:).hash
135
+ # => {
136
+ # "protocol_version" => 970101,
137
+ # "unique_id" => 861106059716756,
138
+ # "device_name" => "GL30MEU",
139
+ # "longitude" => 121.348554,
140
+ # "latitude" => 31.163204,
141
+ # "csq_rssi" => 21,
142
+ # "battery_voltage" => 3552,
143
+ # "movement_status" => 1,
144
+ # ...
145
+ # }
146
+
147
+ # GL30MEU — device info (GTINF)
148
+ response = '970101,867963069921253,,89882280666211671601,24,0,,1,,62,,20251220005654,,,,,,,,20260224104605,1182'
149
+ parsed = QuelinkMg::Gl30meu::Resp::Gtinf.new(response:).hash
150
+ # => {
151
+ # "unique_id" => 867963069921253,
152
+ # "iccid" => "89882280666211671601",
153
+ # "csq_rssi" => 24,
154
+ # "battery_percentage" => 62,
155
+ # ...
156
+ # }
157
+ ```
158
+
159
+ ### Parsing command acknowledgments (ACK)
160
+
161
+ Parse ACK messages the device sends after receiving an AT command:
162
+
163
+ ```ruby
164
+ # GL320M
165
+ response = 'D40102,868239051011356,,READ,0040,20210816045509,004F'
166
+ parsed = QuelinkMg::Ack::Gtrto.new(response:).hash
167
+ # => {
168
+ # "protocol_version" => "D40102",
169
+ # "unique_id" => 868239051011356,
170
+ # "sub_command" => "READ",
171
+ # "serial_number" => "0040",
172
+ # ...
173
+ # }
174
+
175
+ # GL30MEU
176
+ response = '970101,861106059716756,GL30MEU,5,FFFF,20231011084300,0A01'
177
+ parsed = QuelinkMg::Gl30meu::Ack::Gtrto.new(response:).hash
178
+ # => {
179
+ # "unique_id" => 861106059716756,
180
+ # "device_name" => "GL30MEU",
181
+ # "sub_command" => 5,
182
+ # ...
183
+ # }
184
+ ```
185
+
186
+ ### Parsing buffered messages (BUFF)
187
+
188
+ Buffered messages were stored on the device while it was offline. Same interface as RESP:
189
+
190
+ ```ruby
191
+ # GL320M
192
+ parsed = QuelinkMg::Buff::Gtfri.new(response:).hash
193
+
194
+ # GL30MEU
195
+ parsed = QuelinkMg::Gl30meu::Buff::Gtfri.new(response:).hash
196
+ ```
197
+
198
+ ## Available classes
199
+
200
+ ### GL320M
201
+
202
+ | Type | Classes |
203
+ |------|---------|
204
+ | AT commands | `Gtbsi`, `Gtcfg`, `Gtcmd`, `Gtfri`, `Gtqss`, `Gtrto`, `Gtsri`, `Gtudf`, `Gtupd` |
205
+ | Response parsers | `Gtfri`, `Gtgsv`, `Gtinf`, `Gtsos`, `Gtstt`, `Gtupc`, `Gtupd`, `Gtver` |
206
+ | ACK parsers | `Gtbsi`, `Gtcfg`, `Gtcmd`, `Gtfri`, `Gtqss`, `Gtrto`, `Gtsri`, `Gtudf` |
207
+ | Buff parsers | `Gtfri` |
208
+
209
+ ### GL30MEU
210
+
211
+ | Type | Classes |
212
+ |------|---------|
213
+ | AT commands | `Gtbsi`, `Gtcfg`, `Gtrto`, `Gtsri`, `Gtupd` |
214
+ | Response parsers | `Gtati`, `Gtfri`, `Gtinf`, `Gtupc`, `Gtupd` |
215
+ | ACK parsers | `Gtbsi`, `Gtcfg`, `Gtrto`, `Gtsri` |
216
+ | Buff parsers | `Gtfri` |
217
+
18
218
  ## Development
19
219
 
20
- Building gem locally (you can change file name, ofc):
220
+ Building gem locally:
221
+
222
+ ```
21
223
  gem build *.gemspec -o pkg/quelink-mg.gem
224
+ ```
22
225
 
23
226
  Installing:
227
+
228
+ ```
24
229
  gem install pkg/quelink-mg.gem
230
+ ```
231
+
232
+ Running tests:
25
233
 
234
+ ```
235
+ bundle exec rspec
236
+ ```
26
237
 
@@ -11,7 +11,8 @@ module QuelinkMg
11
11
 
12
12
  private
13
13
 
14
- GTUPD_VALID_PARAMS = %i[password subcommand max_download_retry download_timeout download_protocol download_username download_password
14
+ GTUPD_VALID_PARAMS = %i[password subcommand max_download_retry download_timeout download_protocol
15
+ download_username download_password
15
16
  download_url reserved update_type reserved reserved serial_number].freeze
16
17
 
17
18
  def joined_params
@@ -21,7 +22,7 @@ module QuelinkMg
21
22
  def validate_values
22
23
  acceptable_values = {
23
24
  download_retry: (0..3),
24
- download_timeout: (10..30),
25
+ download_timeout: (10..30)
25
26
  }
26
27
 
27
28
  verify_params(acceptable_values, InvalidATGTUPDException)
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QuelinkMg
4
+ module DeviceType
5
+ def self.detect(protocol_version)
6
+ case protocol_version[0..1]
7
+ when 'C3' then :gl320m
8
+ when '97' then :gl30meu
9
+ else :unknown
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QuelinkMg
4
+ module Gl30meu
5
+ module Ack
6
+ class Gtbsi < ::QuelinkMg::Ack::Base
7
+ GTBSI_ACK_KEYS = %w[protocol_version unique_id device_name serial_number send_time count_number].freeze
8
+
9
+ def hash
10
+ unify_keys(GTBSI_ACK_KEYS.zip(@response.split(',')).to_h)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QuelinkMg
4
+ module Gl30meu
5
+ module Ack
6
+ class Gtcfg < ::QuelinkMg::Ack::Base
7
+ GTCFG_ACK_KEYS = %w[protocol_version unique_id device_name serial_number send_time count_number].freeze
8
+
9
+ def hash
10
+ unify_keys(GTCFG_ACK_KEYS.zip(@response.split(',')).to_h)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QuelinkMg
4
+ module Gl30meu
5
+ module Ack
6
+ class Gtrto < ::QuelinkMg::Ack::Base
7
+ GTRTO_ACK_KEYS = %w[protocol_version unique_id device_name sub_command serial_number send_time
8
+ count_number].freeze
9
+
10
+ def hash
11
+ unify_keys(GTRTO_ACK_KEYS.zip(@response.split(',')).to_h)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QuelinkMg
4
+ module Gl30meu
5
+ module Ack
6
+ class Gtsri < ::QuelinkMg::Ack::Base
7
+ GTSRI_ACK_KEYS = %w[protocol_version unique_id device_name serial_number send_time count_number].freeze
8
+
9
+ def hash
10
+ unify_keys(GTSRI_ACK_KEYS.zip(@response.split(',')).to_h)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QuelinkMg
4
+ module Gl30meu
5
+ module At
6
+ class Gtbsi < ::QuelinkMg::At::Base
7
+ def message
8
+ validate_values
9
+
10
+ "AT+GTBSI=#{joined_params}$"
11
+ end
12
+
13
+ private
14
+
15
+ GTBSI_VALID_PARAMS = %i[password lte_apn lte_apn_user_name ltn_apn_password gprs_apn gprs_apn_user_name
16
+ gprs_apn_password network_mode lte_mode apn_authentication_methods
17
+ edrx_periodic edrx_m1_pagings edrx_nb2_pagings
18
+ reserved reserved reserved serial_number].freeze
19
+
20
+ def joined_params
21
+ GTBSI_VALID_PARAMS.map { |method| @params.fetch(method, nil) }.join(',')
22
+ end
23
+
24
+ def validate_values
25
+ acceptable_values = {
26
+ network_mode: (0..3),
27
+ lte_mode: (0..5),
28
+ apn_authentication_methods: (0..3)
29
+ }
30
+
31
+ verify_params(acceptable_values, InvalidATGTBSIException)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QuelinkMg
4
+ module Gl30meu
5
+ module At
6
+ class Gtcfg < ::QuelinkMg::At::Base
7
+ def message
8
+ validate_values
9
+
10
+ "AT+GTCFG=#{joined_params}$"
11
+ end
12
+
13
+ private
14
+
15
+ GTCFG_VALID_PARAMS = %i[password new_password device_name gnss_timeout event_mask report_item_mask
16
+ mode_selection continuous_send_interval reserved start_mode specified_time_of_day
17
+ reserved wakeup_interval reserved reserved reserved gnss_enable agps_mode
18
+ gsm_report reserved reserved battery_low_percentage function_button_mode
19
+ reserved sos_report_mode wifi_report led_on serial_number].freeze
20
+
21
+ def joined_params
22
+ GTCFG_VALID_PARAMS.map { |method| @params.fetch(method, nil) }.join(',')
23
+ end
24
+
25
+ def validate_values
26
+ acceptable_values = {
27
+ gnss_timeout: (5..300),
28
+ mode_selection: (0..5),
29
+ continuous_send_interval: (30..86_400),
30
+ start_mode: (0..3),
31
+ wakeup_interval: (60..86_400),
32
+ gnss_enable: (0..1),
33
+ agps_mode: (0..1),
34
+ battery_low_percentage: (0..30),
35
+ function_button_mode: (0..3),
36
+ sos_report_mode: (0..2),
37
+ wifi_report: (0..1),
38
+ led_on: (0..2)
39
+ }
40
+
41
+ verify_params(acceptable_values, InvalidATGTCFGException)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QuelinkMg
4
+ module Gl30meu
5
+ module At
6
+ class Gtrto < ::QuelinkMg::At::Base
7
+ def message
8
+ validate_values
9
+
10
+ "AT+GTRTO=#{joined_params}$"
11
+ end
12
+
13
+ private
14
+
15
+ GTRTO_VALID_PARAMS = %i[password sub_command single_command_configuration reserved
16
+ reserved reserved sub_command_parameter serial_number].freeze
17
+
18
+ def joined_params
19
+ GTRTO_VALID_PARAMS.map { |method| @params.fetch(method, nil) }.join(',')
20
+ end
21
+
22
+ def validate_values
23
+ acceptable_values = {
24
+ sub_command: (1..7).to_a + [0xB, 0xD, 0x1C, 0x31, 0x33, 0x34]
25
+ }
26
+
27
+ verify_params(acceptable_values, InvalidATGTRTOException)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QuelinkMg
4
+ module Gl30meu
5
+ module At
6
+ class Gtsri < ::QuelinkMg::At::Base
7
+ def message
8
+ validate_values
9
+
10
+ "AT+GTSRI=#{joined_params}$"
11
+ end
12
+
13
+ private
14
+
15
+ GTSRI_VALID_PARAMS = %i[password report_mode reserved buffer_mode main_server_ip main_server_port
16
+ backup_server_ip backup_server_port sms_gateway heartbit_interval sack_enable
17
+ sms_ack_enable reserved reserved reserved serial_number].freeze
18
+
19
+ def joined_params
20
+ GTSRI_VALID_PARAMS.map { |method| @params.fetch(method, nil) }.join(',')
21
+ end
22
+
23
+ def validate_values
24
+ acceptable_values = {
25
+ report_mode: (0..7),
26
+ buffer_mode: (0..2),
27
+ main_server_port: (0..65_535),
28
+ backup_server_port: (0..65_535),
29
+ heartbit_interval: [0] + (5..360).to_a,
30
+ sack_enable: (0..2),
31
+ sms_ack_enable: (0..1)
32
+ }
33
+
34
+ verify_params(acceptable_values, InvalidATGTSRIException)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QuelinkMg
4
+ module Gl30meu
5
+ module At
6
+ class Gtupd < ::QuelinkMg::At::Base
7
+ def message
8
+ validate_values
9
+
10
+ "AT+GTUPD=#{joined_params}$"
11
+ end
12
+
13
+ private
14
+
15
+ GTUPD_VALID_PARAMS = %i[password subcommand max_download_retry download_timeout download_protocol
16
+ download_username download_password download_url reserved update_type
17
+ reserved reserved serial_number].freeze
18
+
19
+ def joined_params
20
+ GTUPD_VALID_PARAMS.map { |method| @params.fetch(method, nil) }.join(',')
21
+ end
22
+
23
+ def validate_values
24
+ acceptable_values = {
25
+ download_retry: (0..3),
26
+ download_timeout: (10..30),
27
+ update_type: [0, 1, 7]
28
+ }
29
+
30
+ verify_params(acceptable_values, InvalidATGTUPDException)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QuelinkMg
4
+ module Gl30meu
5
+ module Buff
6
+ class Gtfri < ::QuelinkMg::Buff::Base
7
+ GTFRI_RESP_KEYS = %w[protocol_version unique_id device_name report_id report_type number
8
+ gps_accuracy speed azimuth elevation longitude latitude gps_utc_time
9
+ mcc mnc lac cell_id csq_rssi csq_ber battery_voltage current_mode_status
10
+ movement_status reserved reserved send_time count_number].freeze
11
+
12
+ def hash
13
+ unify_keys(GTFRI_RESP_KEYS.zip(@response.split(',')).to_h)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QuelinkMg
4
+ module Gl30meu
5
+ module Resp
6
+ class Gtati < ::QuelinkMg::Resp::Base
7
+ GTATI_RESP_KEYS = %w[protocol_version unique_id device_name device_type ati_mask
8
+ firmware_version ble_firmware_version modem_firmware_version
9
+ wifi_firmware_version hardware_version modem_hardware_version
10
+ sensor_id send_time count_number].freeze
11
+
12
+ def hash
13
+ unify_keys(GTATI_RESP_KEYS.zip(@response.split(',')).to_h, true)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QuelinkMg
4
+ module Gl30meu
5
+ module Resp
6
+ class Gtfri < ::QuelinkMg::Resp::Base
7
+ GTFRI_RESP_KEYS = %w[protocol_version unique_id device_name report_id report_type number
8
+ gps_accuracy speed azimuth elevation longitude latitude gps_utc_time
9
+ mcc mnc lac cell_id csq_rssi csq_ber battery_voltage current_mode_status
10
+ movement_status reserved reserved send_time count_number].freeze
11
+
12
+ def hash
13
+ unify_keys(GTFRI_RESP_KEYS.zip(@response.split(',')).to_h)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QuelinkMg
4
+ module Gl30meu
5
+ module Resp
6
+ class Gtinf < ::QuelinkMg::Resp::Base
7
+ GTINF_RESP_KEYS = %w[protocol_version unique_id device_name iccid csq_rssi csq_ber
8
+ reserved mode_selection reserved battery_percentage reserved
9
+ last_gnss_fix_utc_time movement_status
10
+ reserved reserved reserved reserved reserved reserved
11
+ send_time count_number].freeze
12
+
13
+ def hash
14
+ unify_keys(GTINF_RESP_KEYS.zip(@response.split(',')).to_h)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QuelinkMg
4
+ module Gl30meu
5
+ module Resp
6
+ class Gtupc < ::QuelinkMg::Resp::Base
7
+ GTUPC_RESP_KEYS = %w[protocol_version unique_id device_name command_id result download_url send_time
8
+ count_number].freeze
9
+
10
+ def hash
11
+ unify_keys(GTUPC_RESP_KEYS.zip(@response.split(',')).to_h)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QuelinkMg
4
+ module Gl30meu
5
+ module Resp
6
+ class Gtupd < ::QuelinkMg::Resp::Base
7
+ GTUPD_RESP_KEYS = %w[protocol_version unique_id device_name code reserved send_time count_number].freeze
8
+
9
+ def hash
10
+ unify_keys(GTUPD_RESP_KEYS.zip(@response.split(',')).to_h)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -31,7 +31,7 @@ module QuelinkMg
31
31
  Time.use_zone('UTC') { Time.zone.parse(value) }.in_time_zone
32
32
  end
33
33
 
34
- def unify_keys(hash, skip_number_change=false)
34
+ def unify_keys(hash, skip_number_change = false)
35
35
  hash.transform_values do |v|
36
36
  if date?(v)
37
37
  transform_with_timezone(v)
data/lib/quelink_mg.rb CHANGED
@@ -40,6 +40,27 @@ require File.expand_path('quelink-mg/ack/gtudf.rb', __dir__)
40
40
  require File.expand_path('quelink-mg/buff/base.rb', __dir__)
41
41
  require File.expand_path('quelink-mg/buff/gtfri.rb', __dir__)
42
42
 
43
+ require File.expand_path('quelink-mg/device_type.rb', __dir__)
44
+
45
+ require File.expand_path('quelink-mg/gl30meu/at/gtrto.rb', __dir__)
46
+ require File.expand_path('quelink-mg/gl30meu/at/gtcfg.rb', __dir__)
47
+ require File.expand_path('quelink-mg/gl30meu/at/gtbsi.rb', __dir__)
48
+ require File.expand_path('quelink-mg/gl30meu/at/gtsri.rb', __dir__)
49
+ require File.expand_path('quelink-mg/gl30meu/at/gtupd.rb', __dir__)
50
+
51
+ require File.expand_path('quelink-mg/gl30meu/resp/gtfri.rb', __dir__)
52
+ require File.expand_path('quelink-mg/gl30meu/resp/gtati.rb', __dir__)
53
+ require File.expand_path('quelink-mg/gl30meu/resp/gtinf.rb', __dir__)
54
+ require File.expand_path('quelink-mg/gl30meu/resp/gtupc.rb', __dir__)
55
+ require File.expand_path('quelink-mg/gl30meu/resp/gtupd.rb', __dir__)
56
+
57
+ require File.expand_path('quelink-mg/gl30meu/ack/gtrto.rb', __dir__)
58
+ require File.expand_path('quelink-mg/gl30meu/ack/gtcfg.rb', __dir__)
59
+ require File.expand_path('quelink-mg/gl30meu/ack/gtbsi.rb', __dir__)
60
+ require File.expand_path('quelink-mg/gl30meu/ack/gtsri.rb', __dir__)
61
+
62
+ require File.expand_path('quelink-mg/gl30meu/buff/gtfri.rb', __dir__)
63
+
43
64
  require File.expand_path('quelink-mg/configuration.rb', __dir__)
44
65
 
45
66
  module QuelinkMg
data/quelink-mg.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'quelink-mg'
5
- s.version = '0.6.2'
5
+ s.version = '0.7.0'
6
6
  s.summary = 'Quelink devices command reader and writer'
7
7
  s.description = 'Quelink devices command reader and writer'
8
8
  s.authors = ['Stanislaw Zawadzki']
@@ -20,6 +20,6 @@ RSpec.describe QuelinkMg::At::Gtupd do
20
20
  end
21
21
 
22
22
  it 'raises error on wrong params' do
23
- expect { described_class.new(params: { download_timeout: 666}).message }.to raise_error(InvalidATGTUPDException)
23
+ expect { described_class.new(params: { download_timeout: 666 }).message }.to raise_error(InvalidATGTUPDException)
24
24
  end
25
25
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe QuelinkMg::DeviceType do
6
+ describe '.detect' do
7
+ it 'detects GL320M from C3 prefix' do
8
+ expect(described_class.detect('C30204')).to eq :gl320m
9
+ end
10
+
11
+ it 'detects GL30MEU from 97 prefix' do
12
+ expect(described_class.detect('970101')).to eq :gl30meu
13
+ end
14
+
15
+ it 'returns unknown for unrecognized prefix' do
16
+ expect(described_class.detect('XX0101')).to eq :unknown
17
+ end
18
+
19
+ it 'detects GL320M from full GTFRI response' do
20
+ response = 'C30204,860201061504521,,0,0,1,1,0.0,0,96.2,21.012847,52.200338,20230813061232,0260,0003,E31F,0447020D,,34,20230813061231,3E94'
21
+ expect(described_class.detect(response.split(',').first)).to eq :gl320m
22
+ end
23
+
24
+ it 'detects GL30MEU from full GTFRI response' do
25
+ response = '970101,861106059716756,GL30MEU,0,0,1,1,0.0,70,17.8,121.348554,31.163204,20231011084221,0460,0000,5B63,0867349C,21,0,3552,2,1,0,,20231011084241,1A0C'
26
+ expect(described_class.detect(response.split(',').first)).to eq :gl30meu
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe QuelinkMg::Gl30meu::Ack::Gtcfg do
6
+ it 'parses valid GL30MEU GTCFG ACK' do
7
+ response = '970101,861106059716756,GL30MEU,FFFF,20231011084300,0A01'
8
+
9
+ parsed_response = described_class.new(response:).hash
10
+ expect(parsed_response['protocol_version']).to eq 970_101
11
+ expect(parsed_response['unique_id']).to eq 861_106_059_716_756
12
+ expect(parsed_response['device_name']).to eq 'GL30MEU'
13
+ expect(parsed_response['serial_number']).to eq 'FFFF'
14
+ expect(parsed_response['send_time']).to eq Time.use_zone('UTC') { Time.zone.parse('20231011084300') }.in_time_zone
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe QuelinkMg::Gl30meu::Ack::Gtrto do
6
+ it 'parses valid GL30MEU GTRTO ACK' do
7
+ response = '970101,861106059716756,GL30MEU,5,FFFF,20231011084300,0A01'
8
+
9
+ parsed_response = described_class.new(response:).hash
10
+ expect(parsed_response['protocol_version']).to eq 970_101
11
+ expect(parsed_response['unique_id']).to eq 861_106_059_716_756
12
+ expect(parsed_response['device_name']).to eq 'GL30MEU'
13
+ expect(parsed_response['sub_command']).to eq 5
14
+ expect(parsed_response['serial_number']).to eq 'FFFF'
15
+ expect(parsed_response['send_time']).to eq Time.use_zone('UTC') { Time.zone.parse('20231011084300') }.in_time_zone
16
+ end
17
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe QuelinkMg::Gl30meu::At::Gtbsi do
6
+ it 'creates command with eDRX params' do
7
+ params = {
8
+ password: 'gl30',
9
+ lte_apn: 'iot.1nce.net',
10
+ network_mode: 2,
11
+ lte_mode: 2,
12
+ apn_authentication_methods: 0,
13
+ edrx_periodic: '0101',
14
+ edrx_m1_pagings: '0101',
15
+ edrx_nb2_pagings: '0010',
16
+ serial_number: 'FFFF'
17
+ }
18
+
19
+ expect(described_class.new(params:).message).to eq 'AT+GTBSI=gl30,iot.1nce.net,,,,,,2,2,0,0101,0101,0010,,,,FFFF$'
20
+ end
21
+
22
+ it 'creates basic network command' do
23
+ params = {
24
+ password: 'gl30',
25
+ lte_apn: 'iot.1nce.net',
26
+ network_mode: 2,
27
+ lte_mode: 2,
28
+ serial_number: 'FFFF'
29
+ }
30
+
31
+ expect(described_class.new(params:).message).to eq 'AT+GTBSI=gl30,iot.1nce.net,,,,,,2,2,,,,,,,,FFFF$'
32
+ end
33
+
34
+ it 'raises error on missing params' do
35
+ expect { described_class.new(params: {}).message }.to raise_error(InvalidATGTBSIException)
36
+ end
37
+
38
+ it 'raises error on invalid network_mode' do
39
+ params = {
40
+ password: 'gl30',
41
+ network_mode: 5,
42
+ serial_number: 'FFFF'
43
+ }
44
+
45
+ expect { described_class.new(params:).message }.to raise_error(InvalidATGTBSIException)
46
+ end
47
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe QuelinkMg::Gl30meu::At::Gtcfg do
6
+ it 'creates command with continuous_send_interval' do
7
+ params = {
8
+ password: 'gl30',
9
+ continuous_send_interval: 30,
10
+ led_on: 1,
11
+ serial_number: 'FFFF'
12
+ }
13
+
14
+ expect(described_class.new(params:).message).to eq 'AT+GTCFG=gl30,,,,,,,30,,,,,,,,,,,,,,,,,,,1,FFFF$'
15
+ end
16
+
17
+ it 'creates full configuration command' do
18
+ params = {
19
+ password: 'gl30',
20
+ new_password: 'gl30',
21
+ device_name: 'GL30MEU',
22
+ gnss_timeout: 60,
23
+ event_mask: '0FFF',
24
+ report_item_mask: '001F',
25
+ mode_selection: 1,
26
+ continuous_send_interval: 30,
27
+ start_mode: 0,
28
+ wakeup_interval: 300,
29
+ gnss_enable: 1,
30
+ agps_mode: 1,
31
+ gsm_report: '0000',
32
+ battery_low_percentage: 10,
33
+ function_button_mode: 0,
34
+ sos_report_mode: 0,
35
+ wifi_report: 0,
36
+ led_on: 1,
37
+ serial_number: 'FFFF'
38
+ }
39
+
40
+ expect(described_class.new(params:).message).to eq 'AT+GTCFG=gl30,gl30,GL30MEU,60,0FFF,001F,1,30,,0,,,300,,,,1,1,0000,,,10,0,,0,0,1,FFFF$'
41
+ end
42
+
43
+ it 'creates minimal command' do
44
+ params = {
45
+ password: 'gl30',
46
+ gnss_enable: 1,
47
+ serial_number: 'FFFF'
48
+ }
49
+
50
+ expect(described_class.new(params:).message).to eq 'AT+GTCFG=gl30,,,,,,,,,,,,,,,,1,,,,,,,,,,,FFFF$'
51
+ end
52
+
53
+ it 'raises error on missing params' do
54
+ expect { described_class.new(params: {}).message }.to raise_error(InvalidATGTCFGException)
55
+ end
56
+
57
+ it 'raises error on invalid led_on' do
58
+ params = {
59
+ password: 'gl30',
60
+ led_on: 3,
61
+ serial_number: 'FFFF'
62
+ }
63
+
64
+ expect { described_class.new(params:).message }.to raise_error(InvalidATGTCFGException)
65
+ end
66
+
67
+ it 'raises error on continuous_send_interval below minimum' do
68
+ params = {
69
+ password: 'gl30',
70
+ continuous_send_interval: 5,
71
+ serial_number: 'FFFF'
72
+ }
73
+
74
+ expect { described_class.new(params:).message }.to raise_error(InvalidATGTCFGException)
75
+ end
76
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe QuelinkMg::Gl30meu::At::Gtrto do
6
+ it 'creates power off command' do
7
+ params = {
8
+ password: 'gl30',
9
+ sub_command: 5,
10
+ serial_number: 'FFFF'
11
+ }
12
+
13
+ expect(described_class.new(params:).message).to eq 'AT+GTRTO=gl30,5,,,,,,FFFF$'
14
+ end
15
+
16
+ it 'creates reboot command' do
17
+ params = {
18
+ password: 'gl30',
19
+ sub_command: 3,
20
+ serial_number: 'FFFF'
21
+ }
22
+
23
+ expect(described_class.new(params:).message).to eq 'AT+GTRTO=gl30,3,,,,,,FFFF$'
24
+ end
25
+
26
+ it 'creates device info request (0x33)' do
27
+ params = {
28
+ password: 'gl30',
29
+ sub_command: 0x33,
30
+ serial_number: 'FFFF'
31
+ }
32
+
33
+ expect(described_class.new(params:).message).to eq 'AT+GTRTO=gl30,51,,,,,,FFFF$'
34
+ end
35
+
36
+ it 'raises error on missing params' do
37
+ expect { described_class.new(params: {}).message }.to raise_error(InvalidATGTRTOException)
38
+ end
39
+
40
+ it 'raises error on GL320M-only sub_command 0' do
41
+ params = {
42
+ password: 'gl30',
43
+ sub_command: 0,
44
+ serial_number: 'FFFF'
45
+ }
46
+
47
+ expect { described_class.new(params:).message }.to raise_error(InvalidATGTRTOException)
48
+ end
49
+
50
+ it 'raises error on invalid sub_command 0xFF' do
51
+ params = {
52
+ password: 'gl30',
53
+ sub_command: 0xFF,
54
+ serial_number: 'FFFF'
55
+ }
56
+
57
+ expect { described_class.new(params:).message }.to raise_error(InvalidATGTRTOException)
58
+ end
59
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe QuelinkMg::Gl30meu::Buff::Gtfri do
6
+ it 'parses valid GL30MEU buffered GTFRI response' do
7
+ response = '970101,861106059716756,GL30MEU,0,0,1,1,0.0,70,17.8,121.348554,31.163204,20231011084221,0460,0000,5B63,0867349C,21,0,3552,2,1,0,,20231011084241,1A0C'
8
+
9
+ parsed_response = described_class.new(response:).hash
10
+ expect(parsed_response).not_to eq({})
11
+ expect(parsed_response['longitude']).to eq 121.348554
12
+ expect(parsed_response['latitude']).to eq 31.163204
13
+ expect(parsed_response['battery_voltage']).to eq 3552
14
+ expect(parsed_response['csq_rssi']).to eq 21
15
+ expect(parsed_response['movement_status']).to eq 1
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe QuelinkMg::Gl30meu::Resp::Gtati do
6
+ it 'parses valid GL30MEU GTATI response' do
7
+ response = '970101,861106059717499,GL30MEU,88,00043483,0118,0111,0227,0102,0101,0103,33,20231011002150,0AB7'
8
+
9
+ parsed_response = described_class.new(response:).hash
10
+ expect(parsed_response).not_to eq({})
11
+ expect(parsed_response['protocol_version']).to eq '970101'
12
+ expect(parsed_response['unique_id']).to eq '861106059717499'
13
+ expect(parsed_response['device_name']).to eq 'GL30MEU'
14
+ expect(parsed_response['device_type']).to eq '88'
15
+ expect(parsed_response['ati_mask']).to eq '00043483'
16
+ expect(parsed_response['firmware_version']).to eq '0118'
17
+ expect(parsed_response['ble_firmware_version']).to eq '0111'
18
+ expect(parsed_response['modem_firmware_version']).to eq '0227'
19
+ expect(parsed_response['wifi_firmware_version']).to eq '0102'
20
+ expect(parsed_response['hardware_version']).to eq '0101'
21
+ expect(parsed_response['modem_hardware_version']).to eq '0103'
22
+ expect(parsed_response['sensor_id']).to eq '33'
23
+ expect(parsed_response['send_time']).to eq Time.use_zone('UTC') { Time.zone.parse('20231011002150') }.in_time_zone
24
+ expect(parsed_response['count_number']).to eq '0AB7'
25
+ end
26
+
27
+ it 'keeps version fields as strings (skip_number_change)' do
28
+ response = '970101,861106059717499,GL30MEU,88,00043483,0118,0111,0227,0102,0101,0103,33,20231011002150,0AB7'
29
+
30
+ parsed_response = described_class.new(response:).hash
31
+ expect(parsed_response['firmware_version']).to be_a(String)
32
+ expect(parsed_response['hardware_version']).to be_a(String)
33
+ expect(parsed_response['ble_firmware_version']).to be_a(String)
34
+ expect(parsed_response['modem_firmware_version']).to be_a(String)
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe QuelinkMg::Gl30meu::Resp::Gtfri do
6
+ it 'parses valid GL30MEU GTFRI response' do
7
+ response = '970101,861106059716756,GL30MEU,0,0,1,1,0.0,70,17.8,121.348554,31.163204,20231011084221,0460,0000,5B63,0867349C,21,0,3552,2,1,0,,20231011084241,1A0C'
8
+
9
+ parsed_response = described_class.new(response:).hash
10
+ expect(parsed_response).not_to eq({})
11
+ expect(parsed_response['protocol_version']).to eq 970_101
12
+ expect(parsed_response['unique_id']).to eq 861_106_059_716_756
13
+ expect(parsed_response['device_name']).to eq 'GL30MEU'
14
+ expect(parsed_response['longitude']).to eq 121.348554
15
+ expect(parsed_response['latitude']).to eq 31.163204
16
+ expect(parsed_response['speed']).to eq 0.0
17
+ expect(parsed_response['azimuth']).to eq 70
18
+ expect(parsed_response['elevation']).to eq 17.8
19
+ expect(parsed_response['gps_utc_time']).to eq Time.use_zone('UTC') {
20
+ Time.zone.parse('20231011084221')
21
+ }.in_time_zone
22
+ expect(parsed_response['send_time']).to eq Time.use_zone('UTC') { Time.zone.parse('20231011084241') }.in_time_zone
23
+ expect(parsed_response['csq_rssi']).to eq 21
24
+ expect(parsed_response['csq_ber']).to eq 0
25
+ expect(parsed_response['battery_voltage']).to eq 3552
26
+ expect(parsed_response['current_mode_status']).to eq 2
27
+ expect(parsed_response['movement_status']).to eq 1
28
+ end
29
+
30
+ it 'has 25 keys' do
31
+ response = '970101,861106059716756,GL30MEU,0,0,1,1,0.0,70,17.8,121.348554,31.163204,20231011084221,0460,0000,5B63,0867349C,21,0,3552,2,1,0,,20231011084241,1A0C'
32
+
33
+ parsed_response = described_class.new(response:).hash
34
+ expect(parsed_response.keys.length).to eq 25
35
+ end
36
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe QuelinkMg::Gl30meu::Resp::Gtinf do
6
+ it 'parses real GL30MEU GTINF response (21 fields)' do
7
+ response = '970101,867963069921253,,89882280666211671601,24,0,,1,,62,,20251220005654,,,,,,,,20260224104605,1182'
8
+
9
+ parsed_response = described_class.new(response:).hash
10
+ expect(parsed_response).not_to eq({})
11
+ expect(parsed_response['protocol_version']).to eq 970_101
12
+ expect(parsed_response['unique_id']).to eq 867_963_069_921_253
13
+ expect(parsed_response['device_name']).to eq ''
14
+ expect(parsed_response['iccid']).to eq 89_882_280_666_211_671_601
15
+ expect(parsed_response['csq_rssi']).to eq 24
16
+ expect(parsed_response['csq_ber']).to eq 0
17
+ expect(parsed_response['mode_selection']).to eq 1
18
+ expect(parsed_response['battery_percentage']).to eq 62
19
+ expect(parsed_response['last_gnss_fix_utc_time']).to eq Time.use_zone('UTC') {
20
+ Time.zone.parse('20251220005654')
21
+ }.in_time_zone
22
+ expect(parsed_response['movement_status']).to eq ''
23
+ expect(parsed_response['send_time']).to eq Time.use_zone('UTC') { Time.zone.parse('20260224104605') }.in_time_zone
24
+ expect(parsed_response['count_number']).to eq 1182
25
+ end
26
+
27
+ it 'has 13 unique keys (reserved fields collapse)' do
28
+ response = '970101,867963069921253,,89882280666211671601,24,0,,1,,62,,20251220005654,,,,,,,,20260224104605,1182'
29
+
30
+ parsed_response = described_class.new(response:).hash
31
+ expect(parsed_response.keys.length).to eq 13
32
+ end
33
+ end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quelink-mg
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stanislaw Zawadzki
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-08-26 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activesupport
@@ -113,6 +112,22 @@ files:
113
112
  - lib/quelink-mg/buff/base.rb
114
113
  - lib/quelink-mg/buff/gtfri.rb
115
114
  - lib/quelink-mg/configuration.rb
115
+ - lib/quelink-mg/device_type.rb
116
+ - lib/quelink-mg/gl30meu/ack/gtbsi.rb
117
+ - lib/quelink-mg/gl30meu/ack/gtcfg.rb
118
+ - lib/quelink-mg/gl30meu/ack/gtrto.rb
119
+ - lib/quelink-mg/gl30meu/ack/gtsri.rb
120
+ - lib/quelink-mg/gl30meu/at/gtbsi.rb
121
+ - lib/quelink-mg/gl30meu/at/gtcfg.rb
122
+ - lib/quelink-mg/gl30meu/at/gtrto.rb
123
+ - lib/quelink-mg/gl30meu/at/gtsri.rb
124
+ - lib/quelink-mg/gl30meu/at/gtupd.rb
125
+ - lib/quelink-mg/gl30meu/buff/gtfri.rb
126
+ - lib/quelink-mg/gl30meu/resp/gtati.rb
127
+ - lib/quelink-mg/gl30meu/resp/gtfri.rb
128
+ - lib/quelink-mg/gl30meu/resp/gtinf.rb
129
+ - lib/quelink-mg/gl30meu/resp/gtupc.rb
130
+ - lib/quelink-mg/gl30meu/resp/gtupd.rb
116
131
  - lib/quelink-mg/resp/base.rb
117
132
  - lib/quelink-mg/resp/gtfri.rb
118
133
  - lib/quelink-mg/resp/gtgsv.rb
@@ -142,6 +157,16 @@ files:
142
157
  - spec/quelink_mg/at/gtudf_spec.rb
143
158
  - spec/quelink_mg/at/gtupd_spec.rb
144
159
  - spec/quelink_mg/buff/gtfri_spec.rb
160
+ - spec/quelink_mg/device_type_spec.rb
161
+ - spec/quelink_mg/gl30meu/ack/gtcfg_spec.rb
162
+ - spec/quelink_mg/gl30meu/ack/gtrto_spec.rb
163
+ - spec/quelink_mg/gl30meu/at/gtbsi_spec.rb
164
+ - spec/quelink_mg/gl30meu/at/gtcfg_spec.rb
165
+ - spec/quelink_mg/gl30meu/at/gtrto_spec.rb
166
+ - spec/quelink_mg/gl30meu/buff/gtfri_spec.rb
167
+ - spec/quelink_mg/gl30meu/resp/gtati_spec.rb
168
+ - spec/quelink_mg/gl30meu/resp/gtfri_spec.rb
169
+ - spec/quelink_mg/gl30meu/resp/gtinf_spec.rb
145
170
  - spec/quelink_mg/resp/gtfri_spec.rb
146
171
  - spec/quelink_mg/resp/gtgsv_spec.rb
147
172
  - spec/quelink_mg/resp/gtinf_spec.rb
@@ -151,12 +176,10 @@ files:
151
176
  - spec/quelink_mg/resp/gtupd_spec.rb
152
177
  - spec/quelink_mg/resp/gtver_spec.rb
153
178
  - spec/spec_helper.rb
154
- homepage:
155
179
  licenses:
156
180
  - MIT
157
181
  metadata:
158
182
  rubygems_mfa_required: 'false'
159
- post_install_message:
160
183
  rdoc_options: []
161
184
  require_paths:
162
185
  - lib
@@ -171,8 +194,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
171
194
  - !ruby/object:Gem::Version
172
195
  version: '0'
173
196
  requirements: []
174
- rubygems_version: 3.5.18
175
- signing_key:
197
+ rubygems_version: 3.6.9
176
198
  specification_version: 4
177
199
  summary: Quelink devices command reader and writer
178
200
  test_files: []