gts 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/bin/gts +54 -0
- data/gts.gemspec +23 -0
- data/lib/gts/abstract_gps_tracker_handler.rb +21 -0
- data/lib/gts/command.rb +35 -0
- data/lib/gts/command_parser.rb +25 -0
- data/lib/gts/commands/list_known_devices_command.rb +16 -0
- data/lib/gts/commands/uptime_command.rb +15 -0
- data/lib/gts/handlers/tk102_handler.rb +70 -0
- data/lib/gts/server.rb +173 -0
- data/lib/gts/utils/phone_codes.yml +67 -0
- data/lib/gts/utils/phone_system.rb +239 -0
- data/lib/gts/version.rb +3 -0
- data/lib/gts.rb +63 -0
- data/spec/command_spec.rb +15 -0
- data/spec/test_data/tk102_sample_data.txt +20 -0
- data/spec/tk102_handler_spec.rb +21 -0
- metadata +134 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Milan Novota
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Gts
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'gts'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install gts
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/bin/gts
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "gts"
|
4
|
+
require "optparse"
|
5
|
+
|
6
|
+
options = { :port => 2222, :address => "0.0.0.0" }
|
7
|
+
|
8
|
+
opt_parser = OptionParser.new do |opt|
|
9
|
+
opt.banner = "GPS Tracking Server, v.#{Gts::VERSION}, (c) 2012, Milan Novota" +
|
10
|
+
"\nUsage: gts [OPTIONS]"
|
11
|
+
opt.separator ""
|
12
|
+
opt.separator "Options"
|
13
|
+
|
14
|
+
opt.on("-a","--address IP_ADDRESS","on what address should server run") do |address|
|
15
|
+
options[:address] = address
|
16
|
+
end
|
17
|
+
|
18
|
+
opt.on("-p","--port PORT_NUMBER","on what port should server run") do |port|
|
19
|
+
options[:port] = port
|
20
|
+
end
|
21
|
+
|
22
|
+
opt.on("-d","--daemon","run in daemon mode?") do
|
23
|
+
options[:daemon] = true
|
24
|
+
end
|
25
|
+
|
26
|
+
opt.on("-o","--output FILENAME","output file name; if given csv file with captured data is created") do |filename|
|
27
|
+
options[:output_file] = filename
|
28
|
+
end
|
29
|
+
|
30
|
+
opt.on("-x","--debug","show debugging information") do
|
31
|
+
options[:debug] = true
|
32
|
+
end
|
33
|
+
|
34
|
+
opt.on("-h","--help","help") do
|
35
|
+
puts opt_parser
|
36
|
+
exit
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
opt_parser.parse!
|
42
|
+
|
43
|
+
puts "Starting GTS #{Gts::VERSION} on address #{options[:address]} and port #{options[:port]}"
|
44
|
+
if options[:debug]
|
45
|
+
Gts.set_log_level :debug
|
46
|
+
puts "Debugging level logs enabled!"
|
47
|
+
end
|
48
|
+
puts "Known devices: " + Gts.registered_handlers.map{|k,v| [k] }.join(", ")
|
49
|
+
|
50
|
+
Signal.trap("INT") { Gts::Server.stop! }
|
51
|
+
Signal.trap("TERM") { Gts::Server.stop! }
|
52
|
+
|
53
|
+
Gts.server = Gts::Server
|
54
|
+
Gts.server.start!(:address => options[:address], :port => options[:port], :output_file => options[:output_file])
|
data/gts.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/gts/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Milan Novota"]
|
6
|
+
gem.email = ["milan.novota@gmail.com"]
|
7
|
+
gem.description = ["GPS Tracking Server"]
|
8
|
+
gem.summary = ["GPS Tracking Server"]
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "gts"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Gts::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency "eventmachine"
|
19
|
+
gem.add_dependency "json"
|
20
|
+
gem.add_dependency "awesome_print"
|
21
|
+
gem.add_development_dependency "rspec"
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Gts
|
2
|
+
|
3
|
+
class AbstractGPSTrackerHandler
|
4
|
+
|
5
|
+
class CantParseGPSData < StandardError; end
|
6
|
+
|
7
|
+
def self.register!
|
8
|
+
Gts.register_handler(self)
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse(raw_data)
|
12
|
+
raise "Abstract!"
|
13
|
+
end
|
14
|
+
|
15
|
+
def devices
|
16
|
+
raise "Abstract!"
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
data/lib/gts/command.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require "gts/command_parser"
|
2
|
+
|
3
|
+
module Gts
|
4
|
+
|
5
|
+
class Command
|
6
|
+
|
7
|
+
class CommandError < StandardError; end
|
8
|
+
class UnknownCommand < CommandError; end
|
9
|
+
|
10
|
+
@@known_commands = {}
|
11
|
+
attr_reader :args
|
12
|
+
|
13
|
+
def self.register(name, handler)
|
14
|
+
@@known_commands[name] = handler
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.execute(command_str)
|
18
|
+
parser = Gts::CommandParser.new
|
19
|
+
command_name, @args = *parser.parse(command_str)
|
20
|
+
command_handler(command_name.to_sym).execute
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def self.command_handler(command_name)
|
26
|
+
@handler = @@known_commands[command_name]
|
27
|
+
raise UnknownCommand if !@handler
|
28
|
+
@handler.new
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
Dir[File.dirname(__FILE__) +"/commands/*.rb"].each {|file| require file }
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Gts
|
2
|
+
|
3
|
+
class CommandParser
|
4
|
+
|
5
|
+
attr_reader :command, :args
|
6
|
+
|
7
|
+
def iniialize
|
8
|
+
@command = nil
|
9
|
+
@args = []
|
10
|
+
end
|
11
|
+
|
12
|
+
# zatial vieme parsovat iba primitivne veci
|
13
|
+
# status
|
14
|
+
# last_data
|
15
|
+
# known_imeis
|
16
|
+
# uptime
|
17
|
+
def parse(str)
|
18
|
+
command, *@args = str.split(/\s+/)
|
19
|
+
@command = command.to_sym
|
20
|
+
[@command, @args]
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Gts
|
2
|
+
|
3
|
+
class TK102Handler < AbstractGPSTrackerHandler
|
4
|
+
|
5
|
+
def parse(raw_data)
|
6
|
+
raw_data = raw_data.gsub( /(^[\s\t\r\n]+|[\s\t\r\n]+$)/, '' )
|
7
|
+
attrs = raw_data.split(",")
|
8
|
+
raise CantParseGPSData if attrs.size != 18 || attrs[2] != "GPRMC"
|
9
|
+
datetime = parse_datetime attrs[0]
|
10
|
+
begin
|
11
|
+
phone = PhoneSystem::PhoneNumber.new(attrs[1]).long
|
12
|
+
rescue PhoneSystem::PhoneSystemError
|
13
|
+
phone = attrs[1]
|
14
|
+
end
|
15
|
+
gps_time = attrs[3].scan(/(\d{2})(\d{2})(\d{2})\.(\d{3})/).first
|
16
|
+
gps_time = "#{gps_time[0]}:#{gps_time[1]}:#{gps_time[2]}.#{gps_time[3]}"
|
17
|
+
gps_date = attrs[11].scan(/([0-9]{2})([0-9]{2})([0-9]{2})/).first
|
18
|
+
gps_year = Time.now.year.to_s[0..1] + gps_date[2]
|
19
|
+
gps_date = "#{gps_year}-#{gps_date[1]}-#{gps_date[0]}"
|
20
|
+
{
|
21
|
+
:raw => raw_data,
|
22
|
+
:datetime => datetime,
|
23
|
+
:phone => phone,
|
24
|
+
:gps_date => gps_date,
|
25
|
+
:gps_time => gps_time,
|
26
|
+
:gps_signal => (attrs[15] == 'F' ? 'full' : 'low'),
|
27
|
+
:gps_fix => (attrs[4] == 'A' ? 'active' : 'invalid'),
|
28
|
+
:lat => convert_nmea_coordinates(attrs[5], attrs[6]),
|
29
|
+
:lng => convert_nmea_coordinates(attrs[7], attrs[8]),
|
30
|
+
:bearing => attrs[10].to_i,
|
31
|
+
:speed_knots => (attrs[9].to_f * 1000 ).round / 1000,
|
32
|
+
:speed_kmh => ( attrs[9].to_f * 1.852 * 1000 ).round / 1000,
|
33
|
+
:speed_mph => ( attrs[9].to_f * 1.151 * 1000 ).round / 1000,
|
34
|
+
:imei => attrs[16].gsub('imei:', '')
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.devices
|
39
|
+
%w( TK-102 TK-102B )
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# Vrati rozumnejsiu stringovu verziu casu
|
45
|
+
def parse_datetime(str)
|
46
|
+
parts = str.scan(/.{2}/)
|
47
|
+
year = Time.now.year.to_s[0..1] + parts[0]
|
48
|
+
month = parts[1]
|
49
|
+
day = parts[2]
|
50
|
+
hour = parts[3]
|
51
|
+
minute = parts[3]
|
52
|
+
"#{year}-#{month}-#{day} #{hour}:#{minute}"
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def convert_nmea_coordinates(one, two)
|
57
|
+
minutes = one[-7..-1]
|
58
|
+
degrees = one.gsub(minutes, "")
|
59
|
+
one = degrees.to_i + minutes.to_f/60
|
60
|
+
if two == "S" || two == "W"
|
61
|
+
one = -one
|
62
|
+
end
|
63
|
+
one
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
Gts::TK102Handler.register!
|
data/lib/gts/server.rb
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'open-uri'
|
3
|
+
require 'json'
|
4
|
+
require 'gts'
|
5
|
+
|
6
|
+
DEFAULT_IMEI_HANDLERS = { "359585017718724" => "TK-102" } # IMEI nasej TK-102jky
|
7
|
+
DEFAULT_IMEI_HANDLER_SOURCE = nil
|
8
|
+
|
9
|
+
module Gts
|
10
|
+
|
11
|
+
module Server # TCP server
|
12
|
+
|
13
|
+
@@tracker_requests_count = 0
|
14
|
+
@@started_at = nil
|
15
|
+
|
16
|
+
def logger
|
17
|
+
Gts.logger
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.logger
|
21
|
+
Gts.logger
|
22
|
+
end
|
23
|
+
|
24
|
+
class GtsError < StandardError; end;
|
25
|
+
class CantDetermineIMEI < GtsError; end;
|
26
|
+
class DontKnowHowToHandleThisDevice < GtsError; end;
|
27
|
+
|
28
|
+
def initialize
|
29
|
+
@@imei_handlers ||= DEFAULT_IMEI_HANDLERS
|
30
|
+
@@imei_handlers_source ||= DEFAULT_IMEI_HANDLER_SOURCE
|
31
|
+
@@device_handlers = Gts.registered_handlers
|
32
|
+
@@started_at ||= Time.now
|
33
|
+
load_imei_handlers!
|
34
|
+
end
|
35
|
+
|
36
|
+
def post_init
|
37
|
+
logger.info "New connection from #{client_ip_address}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def receive_data data
|
41
|
+
if data =~ /^%/
|
42
|
+
handle_command(data.gsub(/^%\s*?/, ''))
|
43
|
+
else
|
44
|
+
handle_tracker_data(data)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.start!(opts={})
|
49
|
+
@@address = opts[:address]
|
50
|
+
@@port = opts[:port]
|
51
|
+
@@output_file = opts[:output_file]
|
52
|
+
EventMachine::run {
|
53
|
+
EventMachine::start_server @@address, @@port, self
|
54
|
+
puts "Server started. Press Ctrl+C to stop."
|
55
|
+
puts "Hunting for data..."
|
56
|
+
}
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.stop!
|
61
|
+
EventMachine::stop
|
62
|
+
puts "Bye!"
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.uptime
|
66
|
+
sum = Time.now - @@started_at
|
67
|
+
days = (sum / (3600 * 24)).to_i
|
68
|
+
hours = (sum / 3600 - (days * (3600 * 24))).to_i
|
69
|
+
minutes = (sum / 60 - hours * 60).to_i
|
70
|
+
seconds = (sum % 60).to_i
|
71
|
+
"#{days}d #{hours}h #{minutes}m #{seconds}s"
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.list_known_devices
|
75
|
+
@@imei_handlers.map{ |k,v| "#{k}: #{v}" }.join "\n"
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def handle_command(command)
|
81
|
+
begin
|
82
|
+
command = command.strip
|
83
|
+
output = Gts::Command.execute(command)
|
84
|
+
send_data output + "\n"
|
85
|
+
logger.info "Executed '#{command}' [from #{client_ip_address}]"
|
86
|
+
logger.debug "Command output:\n#{output}"
|
87
|
+
rescue Gts::Command::UnknownCommand
|
88
|
+
send_data "Error: Unknown command\n"
|
89
|
+
logger.error "Unknown command '#{command}' [from #{client_ip_address}]"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def handle_tracker_data(data)
|
94
|
+
begin
|
95
|
+
parsed_data = parse_data(data)
|
96
|
+
logger.info "Got correct data from #{client_ip_address}"
|
97
|
+
formatted_data = parsed_data.map{|k,v| "\t#{k}: #{v}\n" }.join
|
98
|
+
logger.debug "Parsed data:\n" + formatted_data
|
99
|
+
log_data_to_csv_file(parsed_data)
|
100
|
+
rescue GtsError => e
|
101
|
+
logger.error "Got incorrect data from #{client_ip_address}. Exception: #{e.to_s}"
|
102
|
+
logger.debug "Incorrect data: #{data}"
|
103
|
+
end
|
104
|
+
close_connection
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
def client_ip_address
|
109
|
+
get_peername[2,6].unpack('nC4')[1,4].join(".")
|
110
|
+
end
|
111
|
+
|
112
|
+
def log_data_to_csv_file(parsed_data)
|
113
|
+
if @@output_file
|
114
|
+
if !File.exists?(@@output_file)
|
115
|
+
File.open(@@output_file, "w") do |f|
|
116
|
+
f.puts parsed_data.keys.map{ |k| k.to_s }.sort.join("\t") # hlavicka
|
117
|
+
end
|
118
|
+
end
|
119
|
+
File.open(@@output_file, "a") do |f|
|
120
|
+
pd = parsed_data.keys.map{ |k| k.to_s }.sort.map{|k| parsed_data[k.to_sym]}.join("\t")
|
121
|
+
f.puts pd
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def parse_data(data)
|
127
|
+
imei = get_imei(data)
|
128
|
+
if !known_imei?(imei)
|
129
|
+
logger.info "Unknown IMEI, reloading IMEI handlers index."
|
130
|
+
load_imei_handlers!
|
131
|
+
end
|
132
|
+
if known_imei?(imei)
|
133
|
+
handler = determine_handler(imei).new
|
134
|
+
handler.parse(data)
|
135
|
+
else
|
136
|
+
raise DontKnowHowToHandleThisDevice, "Don't know what handler to use for IMEI# #{imei}"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def known_imei?(imei)
|
141
|
+
!@@imei_handlers[imei].nil?
|
142
|
+
end
|
143
|
+
|
144
|
+
def determine_handler(imei)
|
145
|
+
imei_handler_str = @@imei_handlers[imei]
|
146
|
+
@@device_handlers[imei_handler_str]
|
147
|
+
end
|
148
|
+
|
149
|
+
# mozno by sme mohli priamo hadat typ zariadenia ked uz hadame imei,
|
150
|
+
# no imei_handlers tabulka a imei je bezpecnejsia cesta
|
151
|
+
def get_imei(data)
|
152
|
+
if data =~ /imei:(\d{15}),/
|
153
|
+
return $1
|
154
|
+
else
|
155
|
+
raise CantDetermineIMEI
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def load_imei_handlers!
|
160
|
+
src = @@imei_handlers_source
|
161
|
+
@@imei_handlers = {} unless @@imei_handlers.is_a? Hash
|
162
|
+
return if src.nil? || src == ""
|
163
|
+
if File.exists?(src)
|
164
|
+
json = File.open(src).read
|
165
|
+
else
|
166
|
+
json = open(src).read
|
167
|
+
end
|
168
|
+
@@imei_handlers.merge!(JSON.parse(json))
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
36:
|
2
|
+
country: Maďarsko
|
3
|
+
area_codes:
|
4
|
+
1: Budapešť
|
5
|
+
48:
|
6
|
+
country: Poľsko
|
7
|
+
area_codes:
|
8
|
+
22: Varšava
|
9
|
+
420:
|
10
|
+
country: Česká republika
|
11
|
+
area_codes:
|
12
|
+
2: Praha
|
13
|
+
5: Brno
|
14
|
+
421:
|
15
|
+
country: Slovensko
|
16
|
+
format: ^(\+|0)[\d\s\(\)-\/]+$
|
17
|
+
min_size: 3
|
18
|
+
max_size: 9
|
19
|
+
area_codes:
|
20
|
+
2: Bratislava
|
21
|
+
31: Dunajská Streda
|
22
|
+
32: Trenčín
|
23
|
+
33: Trnava
|
24
|
+
34: Senica
|
25
|
+
35: Nové Zámky
|
26
|
+
36: Levice
|
27
|
+
37: Nitra
|
28
|
+
38: Topoľčany
|
29
|
+
41: Žilina
|
30
|
+
42: Považská Bystrica
|
31
|
+
43: Martin
|
32
|
+
44: Liptovský Mikuláš
|
33
|
+
45: Zvolen
|
34
|
+
46: Prievidza
|
35
|
+
47: Lučenec
|
36
|
+
48: Banská Bystrica
|
37
|
+
51: Prešov
|
38
|
+
52: Poprad
|
39
|
+
53: Spišská Nová Ves
|
40
|
+
54: Bardejov
|
41
|
+
55: Košice
|
42
|
+
56: Michalovce
|
43
|
+
57: Humenné
|
44
|
+
58: Rožňava
|
45
|
+
901: T-Mobile Slovensko
|
46
|
+
902: T-Mobile Slovensko
|
47
|
+
903: T-Mobile Slovensko
|
48
|
+
904: T-Mobile Slovensko
|
49
|
+
905: Orange Slovensko
|
50
|
+
906: Orange Slovensko (VPN)
|
51
|
+
907: Orange Slovensko
|
52
|
+
908: Orange Slovensko
|
53
|
+
910: T-Mobile Slovensko
|
54
|
+
911: T-Mobile Slovensko
|
55
|
+
912: T-Mobile Slovensko (CVPN)
|
56
|
+
913: Unient Communications
|
57
|
+
914: T-Mobile Slovensko (UMTS)
|
58
|
+
915: Orange Slovensko
|
59
|
+
916: Orange Slovensko (VPN)
|
60
|
+
917: Orange Slovensko (UMTS)
|
61
|
+
918: Orange Slovensko
|
62
|
+
919: Orange Slovensko
|
63
|
+
940: Telefónica O2 Slovakia
|
64
|
+
944: Telefónica O2 Slovakia
|
65
|
+
948: Telefónica O2 Slovakia
|
66
|
+
949: Telefónica O2 Slovakia
|
67
|
+
959: ŽSR (železničná mobilná sieť)
|
@@ -0,0 +1,239 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
module PhoneSystem
|
4
|
+
|
5
|
+
# Exceptions
|
6
|
+
class PhoneSystemError < StandardError; end
|
7
|
+
class CannotParsePhoneNumberString < PhoneSystemError; end
|
8
|
+
class CannotDetermineCountryCode < CannotParsePhoneNumberString; end
|
9
|
+
class AreaCodeNotValidForGivenCountry < CannotParsePhoneNumberString; end
|
10
|
+
class NumberTooShort < CannotParsePhoneNumberString; end
|
11
|
+
class NumberTooLong < CannotParsePhoneNumberString; end
|
12
|
+
|
13
|
+
# For future use, not implemented, yet
|
14
|
+
class PhoneNode; end
|
15
|
+
class PhoneCountryNode < PhoneNode; end
|
16
|
+
class PhoneAreaNode < PhoneNode; end
|
17
|
+
|
18
|
+
class Parser
|
19
|
+
|
20
|
+
@@phone_codes_file_path = File.join(File.dirname(__FILE__), "phone_codes.yml")
|
21
|
+
@@phone_codes = YAML.load(File.read(@@phone_codes_file_path))
|
22
|
+
|
23
|
+
def parse(pstring, options)
|
24
|
+
self.class.parse(pstring, options)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Return hash, that represents valid international phone number with
|
28
|
+
# country code, area code, number, country, area/operator.
|
29
|
+
#
|
30
|
+
# String that do not begin with "+" or "0" are considered invalid.
|
31
|
+
# because we want to get absolute phone numbers, not relative, ie.
|
32
|
+
# the output will contain country code, area/mobile operator code and number.
|
33
|
+
#
|
34
|
+
# Country code is by default set to 421 (Slovakia), so we can leave it
|
35
|
+
# when inputting Slovak phone numbers and just start with area code.
|
36
|
+
#
|
37
|
+
# Constraints:
|
38
|
+
# - the number that do not start with "+" or "0" are considered invalid
|
39
|
+
# (since we are not able to determine the area/operator code).
|
40
|
+
# - the country code MUST be separated from the rest of the number by means
|
41
|
+
# of " " or "-".
|
42
|
+
#
|
43
|
+
# Arguments:
|
44
|
+
# - pstring : string with phone number to parse
|
45
|
+
# options:
|
46
|
+
# - country_code : implicit country phone code, if none found
|
47
|
+
# in pstring, defaults to 421 (Slovakia)
|
48
|
+
# - strict : if set to true, the pstring is checked against the list of
|
49
|
+
# area/operator codes found in phone_codes.yml. If none of the area/operator
|
50
|
+
# code matches pstring, exception is raised. Use only if you have complete
|
51
|
+
# list of the area/operator codes for given country. Defaults to false.
|
52
|
+
def self.parse(pstring, options = {})
|
53
|
+
|
54
|
+
options = {
|
55
|
+
:country_code => 421, # Slovakia
|
56
|
+
:strict => false
|
57
|
+
}.merge(options)
|
58
|
+
|
59
|
+
text_number = pstring
|
60
|
+
# if it starts with "+" or "00" then the country code should be given and we will
|
61
|
+
# try to get it.
|
62
|
+
if pstring =~ /^(\+|00)/
|
63
|
+
cc_match = pstring.match(/^(?:\+|00)(\d*)[ -\(]+/)
|
64
|
+
unless cc_match
|
65
|
+
then raise(CannotParsePhoneNumberString, "The string \"#{text_number}\" does not seem to be valid international phone number."); end
|
66
|
+
pstring = pstring.match(/^[\+0\d]*[ -\(]+(.*)$/)[1] # chop the country code off
|
67
|
+
country_code = cc_match[1].to_i
|
68
|
+
else
|
69
|
+
# if the country code is not set, fall to default
|
70
|
+
# and chop the first zero, so the area/operator code will be at the first place
|
71
|
+
unless options[:country_code]
|
72
|
+
then raise(CannotDetermineCountryCode, "The string \"#{text_number}\" does not seem to be valid phone number. Cannot determine country code."); end
|
73
|
+
pstring = pstring.sub("0","") # remove the leading zero, to get the area code to the first place
|
74
|
+
country_code = options[:country_code]
|
75
|
+
end
|
76
|
+
|
77
|
+
# do some cleaning - remove all the remaining characters, that are not digits
|
78
|
+
pstring = remove_other_than_digits(pstring)
|
79
|
+
|
80
|
+
area_code = nil
|
81
|
+
area_name = nil
|
82
|
+
|
83
|
+
# let's try to identify the country, area/operator and get the correct format for the country
|
84
|
+
# if possible
|
85
|
+
if @@phone_codes.include? country_code
|
86
|
+
country = @@phone_codes[country_code]["country"]
|
87
|
+
area_codes = @@phone_codes[country_code]["area_codes"]
|
88
|
+
pn_format = Regexp.new(@@phone_codes[country_code]["format"]) if @@phone_codes[country_code].include? "format"
|
89
|
+
area_codes.each do |code, name|
|
90
|
+
if pstring =~ /^#{code}/
|
91
|
+
area_code = code
|
92
|
+
area_name = name
|
93
|
+
pstring = pstring.sub(/#{code}/, "") # chop the area code off
|
94
|
+
break
|
95
|
+
end
|
96
|
+
end
|
97
|
+
# if not area code found and in strict mode, raise exception
|
98
|
+
if !area_code && options[:strict]
|
99
|
+
then raise(AreaCodeNotValidForGivenCountry, "The string \"#{text_number}\" does not seem to be valid phone number. Area code was not recognized."); end
|
100
|
+
|
101
|
+
# set the number size boundaries if found for given country
|
102
|
+
min_size = @@phone_codes[country_code].include?("min_size") ? @@phone_codes[country_code]["min_size"] : nil
|
103
|
+
max_size = @@phone_codes[country_code].include?("max_size") ? @@phone_codes[country_code]["max_size"] : nil
|
104
|
+
end
|
105
|
+
|
106
|
+
# if not specific format of numbers for given country set, then
|
107
|
+
# use general format
|
108
|
+
pn_format ||= /^(\+|0)[\d\s\(\)-\/]+$/
|
109
|
+
unless text_number =~ pn_format
|
110
|
+
then raise(CannotParsePhoneNumberString, "The string \"#{text_number}\" does not seem to be valid international phone number."); end
|
111
|
+
|
112
|
+
# if no size boundaries set, set them to reasonable defaults
|
113
|
+
min_size ||= 6
|
114
|
+
max_size ||= 15
|
115
|
+
# and check the remaining number against them
|
116
|
+
if pstring.size < min_size
|
117
|
+
then raise(NumberTooShort, "Cannot parse the string \"#{text_number}\" to phone number. The phone number would be too short."); end
|
118
|
+
if pstring.size > max_size
|
119
|
+
then raise(NumberTooLong, "Cannot parse the string \"#{text_number}\" to phone number. The phone number would be too long."); end
|
120
|
+
|
121
|
+
# if we made it here, we have the line number
|
122
|
+
number = pstring
|
123
|
+
|
124
|
+
out = {
|
125
|
+
:text => text_number,
|
126
|
+
:country_code => country_code,
|
127
|
+
:country => country,
|
128
|
+
:area_code => area_code,
|
129
|
+
:area_name => area_name,
|
130
|
+
:number => number
|
131
|
+
}
|
132
|
+
return out
|
133
|
+
rescue PhoneSystemError
|
134
|
+
false
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
# my way to remove other chars than digits from a string
|
140
|
+
# any better idea?
|
141
|
+
def self.remove_other_than_digits(numbers_and_stuff)
|
142
|
+
out = []
|
143
|
+
numbers_and_stuff.each_char{|c| out << c if c=~/\d/ }
|
144
|
+
out.join
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
class PhoneNumber
|
150
|
+
|
151
|
+
def self.valid?(pstring, options = {})
|
152
|
+
!!PhoneSystem::Parser.parse(pstring, options)
|
153
|
+
end
|
154
|
+
|
155
|
+
def valid?
|
156
|
+
PhoneNumber.valid? self.long
|
157
|
+
end
|
158
|
+
|
159
|
+
def initialize(pstring = nil, options = {})
|
160
|
+
@phone_number = {}
|
161
|
+
@phone_number = PhoneSystem::Parser.parse(pstring,options) unless (pstring == "" || pstring.nil?)
|
162
|
+
end
|
163
|
+
|
164
|
+
def country_code
|
165
|
+
@phone_number[:country_code]
|
166
|
+
end
|
167
|
+
|
168
|
+
def country
|
169
|
+
@phone_number[:country]
|
170
|
+
end
|
171
|
+
|
172
|
+
def area_code
|
173
|
+
@phone_number[:area_code]
|
174
|
+
end
|
175
|
+
|
176
|
+
def area_name
|
177
|
+
@phone_number[:area_name]
|
178
|
+
end
|
179
|
+
|
180
|
+
def area_name
|
181
|
+
@phone_number[:area_name]
|
182
|
+
end
|
183
|
+
|
184
|
+
def number
|
185
|
+
@phone_number[:number]
|
186
|
+
end
|
187
|
+
|
188
|
+
def text
|
189
|
+
@phone_number[:text]
|
190
|
+
end
|
191
|
+
|
192
|
+
def [](attr)
|
193
|
+
@phone_number[attr]
|
194
|
+
end
|
195
|
+
|
196
|
+
def long
|
197
|
+
"+#{country_code} #{area_code} #{number}"
|
198
|
+
end
|
199
|
+
|
200
|
+
def self.parse_and_format_long(pstring, options={})
|
201
|
+
if nr = PhoneSystem::Parser.parse(pstring, options)
|
202
|
+
return "+#{nr[:country_code]} #{nr[:area_code]} #{nr[:number]}"
|
203
|
+
else
|
204
|
+
return false
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def short
|
209
|
+
"0#{area_code}/#{number}"
|
210
|
+
end
|
211
|
+
|
212
|
+
def text=(pstring)
|
213
|
+
@phone_number = PhoneSystem::Parser.new.parse(pstring)
|
214
|
+
end
|
215
|
+
|
216
|
+
def to_hash
|
217
|
+
{
|
218
|
+
:text => text,
|
219
|
+
:country_code => country_code,
|
220
|
+
:country => country,
|
221
|
+
:area_code => area_code,
|
222
|
+
:area_name => area_name,
|
223
|
+
:number => number,
|
224
|
+
:long => long,
|
225
|
+
:short => short
|
226
|
+
}
|
227
|
+
end
|
228
|
+
|
229
|
+
def ==(other_phone_number)
|
230
|
+
self.long = other_phone_number.long
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|
234
|
+
|
235
|
+
end
|
236
|
+
|
237
|
+
# pn = PhoneSystem::PhoneNumber.new "0908420120"
|
238
|
+
# puts pn.inspect
|
239
|
+
|
data/lib/gts/version.rb
ADDED
data/lib/gts.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require "logger"
|
2
|
+
|
3
|
+
module Gts
|
4
|
+
|
5
|
+
def self.register_handler(klass)
|
6
|
+
@@registered_handlers ||= {}
|
7
|
+
klass.devices.each do |d|
|
8
|
+
@@registered_handlers[d] = klass
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.registered_handlers
|
13
|
+
@@registered_handlers
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.set_log_level(level=:info)
|
17
|
+
case level
|
18
|
+
when :debug
|
19
|
+
@@log_level = Logger::DEBUG
|
20
|
+
else
|
21
|
+
@@log_level = Logger::INFO
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.log_level
|
26
|
+
begin
|
27
|
+
@@log_level
|
28
|
+
rescue
|
29
|
+
set_log_level
|
30
|
+
@@log_level
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.logger
|
35
|
+
begin
|
36
|
+
@@logger
|
37
|
+
rescue
|
38
|
+
@@logger = Logger.new(STDOUT)
|
39
|
+
@@logger.datetime_format = "%d.%m.%Y %H:%M:%S"
|
40
|
+
@@logger.formatter = proc do |severity, datetime, progname, msg|
|
41
|
+
"#{datetime} #{severity}: #{msg}\n"
|
42
|
+
end
|
43
|
+
@@logger.level = log_level
|
44
|
+
@@logger
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.server=(server_instance)
|
49
|
+
@@server = server_instance
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.server
|
53
|
+
@@server
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
require "gts/version"
|
59
|
+
require "gts/utils/phone_system"
|
60
|
+
require "gts/abstract_gps_tracker_handler"
|
61
|
+
require "gts/handlers/tk102_handler"
|
62
|
+
require "gts/command"
|
63
|
+
require "gts/server"
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
require "gts"
|
3
|
+
|
4
|
+
describe Gts::Command do
|
5
|
+
|
6
|
+
class SomeCommand < Gts::Command
|
7
|
+
end
|
8
|
+
|
9
|
+
it "allows to register new commands" do
|
10
|
+
expect {
|
11
|
+
SomeCommand.register :some, SomeCommand
|
12
|
+
}.not_to raise_exception
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
1207272308,0907532880,GPRMC,220856.000,A,4809.6585,N,01708.0085,E,0.00,,270712,,,A*72,F,imei:359585017718724,98&?
|
2
|
+
1207272309,0907532880,GPRMC,220957.000,A,4809.6526,N,01707.9981,E,0.00,,270712,,,A*70,F,imei:359585017718724,98?n
|
3
|
+
1207272311,0907532880,GPRMC,221108.000,A,4809.6540,N,01708.0022,E,2.32,,270712,,,A*76,F,imei:359585017718724,98|?
|
4
|
+
1207272311,0907532880,GPRMC,205559.000,A,4809.6760,N,01708.0389,E,0.00,,270712,,,A*71,L,imei:359585017718724,98??
|
5
|
+
1207272315,0907532880,GPRMC,221510.000,A,4809.7429,N,01707.9160,E,7.81,352.81,270712,,,A*6B,F,imei:359585017718724,104?
|
6
|
+
1207272316,0907532880,GPRMC,221634.000,A,4809.6799,N,01707.8709,E,1.60,208.30,270712,,,A*62,F,imei:359585017718724,104?/
|
7
|
+
1207272317,0907532880,GPRMC,221751.000,A,4809.6481,N,01707.8410,E,2.20,229.80,270712,,,A*6E,F,imei:359585017718724,104?
|
8
|
+
1207272318,0907532880,GPRMC,221846.000,A,4809.6214,N,01707.8074,E,2.42,240.02,270712,,,A*6A,F,imei:359585017718724,104?
|
9
|
+
1207272320,0907532880,GPRMC,222001.000,A,4809.5866,N,01707.7769,E,0.79,65.09,270712,,,A*5E,F,imei:359585017718724,103u?
|
10
|
+
1207272321,0907532880,GPRMC,222120.000,A,4809.5363,N,01707.7398,E,4.65,185.29,270712,,,A*6C,F,imei:359585017718724,104X?
|
11
|
+
1207272322,0907532880,GPRMC,222221.000,A,4809.5087,N,01707.7280,E,0.00,291.23,270712,,,A*64,F,imei:359585017718724,104V?
|
12
|
+
1207272323,0907532880,GPRMC,222335.000,A,4809.4998,N,01707.8916,E,1.16,140.35,270712,,,A*63,F,imei:359585017718724,104ݙ
|
13
|
+
1207272325,0907532880,GPRMC,222505.000,A,4809.4882,N,01707.8748,E,5.91,74.16,270712,,,A*55,F,imei:359585017718724,103O
|
14
|
+
1207272326,0907532880,GPRMC,222605.000,A,4809.5271,N,01707.7184,E,11.52,28.57,270712,,,A*6E,F,imei:359585017718724,104?}
|
15
|
+
1207272327,0907532880,GPRMC,222733.000,A,4809.5048,N,01707.7301,E,1.41,,270712,,,A*78,F,imei:359585017718724,98`?
|
16
|
+
1207272329,0907532880,GPRMC,222952.000,A,4809.4744,N,01707.6298,E,0.00,253.21,270712,,,A*66,F,imei:359585017718724,104??
|
17
|
+
1207272330,0907532880,GPRMC,223049.000,A,4809.4793,N,01707.5970,E,1.76,,270712,,,A*79,F,imei:359585017718724,98?
|
18
|
+
1207272331,0907532880,GPRMC,223124.000,A,4809.4821,N,01707.5665,E,1.10,,270712,,,A*7E,F,imei:359585017718724,98E\330
|
19
|
+
1207272332,0907532880,GPRMC,223202.000,A,4809.4943,N,01707.5553,E,2.32,342.78,270712,,,A*6D,F,imei:359585017718724,104\021\370\n
|
20
|
+
1207272332,0907532880,GPRMC,223239.000,A,4809.4987,N,01707.5872,E,1.92,34.17,270712,,,A*51,F,imei:359585017718724,103C
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
require "gts"
|
3
|
+
|
4
|
+
CORRECT_LINE = "1207272321,0907532880,GPRMC,222120.000,A,4809.5363,N,01707.7398,E,4.65,185.29,270712,,,A*6C,F,imei:359585017718724,104X?"
|
5
|
+
INCORRECT_LINE = "blah"
|
6
|
+
|
7
|
+
describe Gts::TK102Handler do
|
8
|
+
|
9
|
+
let(:handler) { Gts::TK102Handler.new }
|
10
|
+
|
11
|
+
it "parses GPS data when data is correct" do
|
12
|
+
handler.parse(CORRECT_LINE).should be_a Hash
|
13
|
+
end
|
14
|
+
|
15
|
+
it "throws an exception when GPS data is incorrect" do
|
16
|
+
expect {
|
17
|
+
handler.parse(INCORRECT_LINE)
|
18
|
+
}.to raise_exception(Gts::TK102Handler::CantParseGPSData)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gts
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Milan Novota
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-07-29 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: eventmachine
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: json
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: awesome_print
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rspec
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
description: ! '["GPS Tracking Server"]'
|
79
|
+
email:
|
80
|
+
- milan.novota@gmail.com
|
81
|
+
executables:
|
82
|
+
- gts
|
83
|
+
extensions: []
|
84
|
+
extra_rdoc_files: []
|
85
|
+
files:
|
86
|
+
- .gitignore
|
87
|
+
- Gemfile
|
88
|
+
- LICENSE
|
89
|
+
- README.md
|
90
|
+
- Rakefile
|
91
|
+
- bin/gts
|
92
|
+
- gts.gemspec
|
93
|
+
- lib/gts.rb
|
94
|
+
- lib/gts/abstract_gps_tracker_handler.rb
|
95
|
+
- lib/gts/command.rb
|
96
|
+
- lib/gts/command_parser.rb
|
97
|
+
- lib/gts/commands/list_known_devices_command.rb
|
98
|
+
- lib/gts/commands/uptime_command.rb
|
99
|
+
- lib/gts/handlers/tk102_handler.rb
|
100
|
+
- lib/gts/server.rb
|
101
|
+
- lib/gts/utils/phone_codes.yml
|
102
|
+
- lib/gts/utils/phone_system.rb
|
103
|
+
- lib/gts/version.rb
|
104
|
+
- spec/command_spec.rb
|
105
|
+
- spec/test_data/tk102_sample_data.txt
|
106
|
+
- spec/tk102_handler_spec.rb
|
107
|
+
homepage: ''
|
108
|
+
licenses: []
|
109
|
+
post_install_message:
|
110
|
+
rdoc_options: []
|
111
|
+
require_paths:
|
112
|
+
- lib
|
113
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - ! '>='
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
|
+
none: false
|
121
|
+
requirements:
|
122
|
+
- - ! '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
requirements: []
|
126
|
+
rubyforge_project:
|
127
|
+
rubygems_version: 1.8.19
|
128
|
+
signing_key:
|
129
|
+
specification_version: 3
|
130
|
+
summary: ! '["GPS Tracking Server"]'
|
131
|
+
test_files:
|
132
|
+
- spec/command_spec.rb
|
133
|
+
- spec/test_data/tk102_sample_data.txt
|
134
|
+
- spec/tk102_handler_spec.rb
|