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.
- checksums.yaml +4 -4
- data/.rubocop.yml +6 -0
- data/Gemfile.lock +2 -1
- data/README.md +218 -7
- data/lib/quelink-mg/at/gtupd.rb +3 -2
- data/lib/quelink-mg/device_type.rb +13 -0
- data/lib/quelink-mg/gl30meu/ack/gtbsi.rb +15 -0
- data/lib/quelink-mg/gl30meu/ack/gtcfg.rb +15 -0
- data/lib/quelink-mg/gl30meu/ack/gtrto.rb +16 -0
- data/lib/quelink-mg/gl30meu/ack/gtsri.rb +15 -0
- data/lib/quelink-mg/gl30meu/at/gtbsi.rb +36 -0
- data/lib/quelink-mg/gl30meu/at/gtcfg.rb +46 -0
- data/lib/quelink-mg/gl30meu/at/gtrto.rb +32 -0
- data/lib/quelink-mg/gl30meu/at/gtsri.rb +39 -0
- data/lib/quelink-mg/gl30meu/at/gtupd.rb +35 -0
- data/lib/quelink-mg/gl30meu/buff/gtfri.rb +18 -0
- data/lib/quelink-mg/gl30meu/resp/gtati.rb +18 -0
- data/lib/quelink-mg/gl30meu/resp/gtfri.rb +18 -0
- data/lib/quelink-mg/gl30meu/resp/gtinf.rb +19 -0
- data/lib/quelink-mg/gl30meu/resp/gtupc.rb +16 -0
- data/lib/quelink-mg/gl30meu/resp/gtupd.rb +15 -0
- data/lib/quelink-mg/resp/base.rb +1 -1
- data/lib/quelink_mg.rb +21 -0
- data/quelink-mg.gemspec +1 -1
- data/spec/quelink_mg/at/gtupd_spec.rb +1 -1
- data/spec/quelink_mg/device_type_spec.rb +29 -0
- data/spec/quelink_mg/gl30meu/ack/gtcfg_spec.rb +16 -0
- data/spec/quelink_mg/gl30meu/ack/gtrto_spec.rb +17 -0
- data/spec/quelink_mg/gl30meu/at/gtbsi_spec.rb +47 -0
- data/spec/quelink_mg/gl30meu/at/gtcfg_spec.rb +76 -0
- data/spec/quelink_mg/gl30meu/at/gtrto_spec.rb +59 -0
- data/spec/quelink_mg/gl30meu/buff/gtfri_spec.rb +17 -0
- data/spec/quelink_mg/gl30meu/resp/gtati_spec.rb +36 -0
- data/spec/quelink_mg/gl30meu/resp/gtfri_spec.rb +36 -0
- data/spec/quelink_mg/gl30meu/resp/gtinf_spec.rb +33 -0
- metadata +29 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: da1518d252576d756cd72b6f4e4a6a0d3e8a0b6966036d83aad656c15de06480
|
|
4
|
+
data.tar.gz: 3485cf01a3a011efcf5463c8f0104359697cbbe5f6d9834b7d11280a0e25f12b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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
|
-
|
|
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
|
-
|
|
9
|
+
```
|
|
10
|
+
gem install quelink-mg
|
|
11
|
+
```
|
|
9
12
|
|
|
10
13
|
Alternatively, you can install the gem with bundler:
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
|
data/lib/quelink-mg/at/gtupd.rb
CHANGED
|
@@ -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
|
|
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,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
|
data/lib/quelink-mg/resp/base.rb
CHANGED
|
@@ -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
|
@@ -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.
|
|
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:
|
|
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.
|
|
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: []
|