tacview_client 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.gitlab-ci.yml +26 -0
- data/.jrubyrc +1 -0
- data/.rspec +1 -0
- data/.rubocop.yml +19 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +77 -0
- data/Rakefile +11 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/exe/connect_tacview +114 -0
- data/lib/tacview_client.rb +8 -0
- data/lib/tacview_client/base_processor.rb +54 -0
- data/lib/tacview_client/client.rb +123 -0
- data/lib/tacview_client/reader.rb +96 -0
- data/lib/tacview_client/reader/parser.rb +96 -0
- data/lib/tacview_client/version.rb +5 -0
- data/tacview_client.gemspec +35 -0
- metadata +164 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bcfff1957de3576841382d36257799ee9ff48131bc8cf723ef8272f01b12ef05
|
4
|
+
data.tar.gz: a9a5d31d8ab4564a097b5be6d6e25358596438b7564aeb41147d8a56ccea130d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e4038988c5541560159fdc78214d166630e4c1d7d8101622b2fcf3edbbe0e3ee5bd7c9085412613e7be1c2debf3591d18346fe66d29aeeaebffd6eb20edb39bf
|
7
|
+
data.tar.gz: 2587a4997a39c703d8b1358a8b2f98565d3165e6433b27c8a03664938a3cc4ceada21ef2e805663d1f01130c54c95a164fd289e6474f519cf9351aff181f246b
|
data/.gitignore
ADDED
data/.gitlab-ci.yml
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
.tests:
|
2
|
+
image: "ruby:2.5"
|
3
|
+
before_script:
|
4
|
+
- apt-get update && apt-get install -y git
|
5
|
+
- ruby -v
|
6
|
+
- which ruby
|
7
|
+
- gem install bundler --no-document
|
8
|
+
- bundle install --jobs $(nproc) "${FLAGS[@]}"
|
9
|
+
|
10
|
+
.rspec:
|
11
|
+
extends: .tests
|
12
|
+
script:
|
13
|
+
- bundle exec rake spec
|
14
|
+
|
15
|
+
rubocop:
|
16
|
+
extends: .tests
|
17
|
+
script:
|
18
|
+
- bundle exec rake rubocop
|
19
|
+
|
20
|
+
rspec-ruby:
|
21
|
+
extends: .rspec
|
22
|
+
image: ruby:latest
|
23
|
+
|
24
|
+
rspec-jruby:
|
25
|
+
extends: .rspec
|
26
|
+
image: jruby:latest
|
data/.jrubyrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
debug.fullTrace=true
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Style/BlockComments:
|
2
|
+
Exclude:
|
3
|
+
# This is the default rspec generated file so leave it be for consistency
|
4
|
+
- spec/spec_helper.rb
|
5
|
+
Layout/CommentIndentation:
|
6
|
+
Exclude:
|
7
|
+
# This is the default rspec generated file so leave it be for consistency
|
8
|
+
- spec/spec_helper.rb
|
9
|
+
|
10
|
+
Metrics/BlockLength:
|
11
|
+
Exclude:
|
12
|
+
# Rubocop does not like the rspec describe block style which is always long
|
13
|
+
- spec/**/*_spec.rb
|
14
|
+
|
15
|
+
Lint/UnusedMethodArgument:
|
16
|
+
Exclude:
|
17
|
+
# This is Base class that acts as a guide so we don't care about unused
|
18
|
+
# parameters
|
19
|
+
- lib/tacview_client/base_processor.rb
|
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
tacview_client
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.6.3
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2019 Jeffrey Jones
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# Tacview Client
|
2
|
+
|
3
|
+
A Ruby client that speaks the [Tacview](http://www.tacview.net) protocol and
|
4
|
+
can connect to a Tacview compatible server.
|
5
|
+
|
6
|
+
This Gem is tested against the latest non-preview releases of Ruby and JRuby.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'tacview_client'
|
14
|
+
```
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
$ bundle
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
$ gem install tacview_client
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
### API
|
27
|
+
|
28
|
+
To connect to a Tacview server with this library you can use the following code
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
|
32
|
+
require 'tacview_client'
|
33
|
+
|
34
|
+
# See docs for all parameters
|
35
|
+
client = TacviewClient::Client.new host: 127.0.0.1,
|
36
|
+
processor: ProcessorInstance
|
37
|
+
```
|
38
|
+
|
39
|
+
This gem uses Inversion Of Control principles and expects you to provide an
|
40
|
+
object to receive events for processing. This can be a `Module` or a `Class`
|
41
|
+
and it must implement the methods descibed in the `BaseProcessor` class. See
|
42
|
+
the documentation of this class for more information.
|
43
|
+
|
44
|
+
For an example of this see the `ConsoleOutputter` module in the
|
45
|
+
`exe/connect_tacview` file
|
46
|
+
|
47
|
+
#### Logging
|
48
|
+
|
49
|
+
This gem provides no logging out of the box. If you want to add logging then
|
50
|
+
the recommended approach is to create your own logger class and use
|
51
|
+
`Module.prepend` to prepend it to the `Client` class (For logging connection
|
52
|
+
information) or the `Reader` class's `#route_line` method for logging of the
|
53
|
+
raw ACMI lines
|
54
|
+
|
55
|
+
For an example of this see the `TacviewClientLogger` module in the
|
56
|
+
`exe/connect_tacview` file
|
57
|
+
|
58
|
+
### Command-line
|
59
|
+
|
60
|
+
This gem provides a command-line application. Use `connect_tacview --help` for
|
61
|
+
invocation information. Thie command-line will connect to a Tacview server and
|
62
|
+
print some connection debug information followed by the parsed event stream to
|
63
|
+
the terminal
|
64
|
+
|
65
|
+
## Development
|
66
|
+
|
67
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then run
|
68
|
+
`rake` to run rubocop and the tests. You can also run `bin/console` for an
|
69
|
+
interactive prompt that will allow you to experiment.
|
70
|
+
|
71
|
+
## Contributing
|
72
|
+
|
73
|
+
Bug reports and pull requests are welcome on [GitLab](https://gitlab.com/overlord-bot/tacview-ruby-client).
|
74
|
+
|
75
|
+
## License
|
76
|
+
|
77
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'tacview_client'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require 'irb'
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/exe/connect_tacview
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'optparse'
|
5
|
+
require 'bundler/setup'
|
6
|
+
require 'tacview_client'
|
7
|
+
require 'tacview_client/base_processor'
|
8
|
+
|
9
|
+
options = {
|
10
|
+
password: nil,
|
11
|
+
port: 42_674,
|
12
|
+
client_name: 'ruby_tacview_client'
|
13
|
+
}
|
14
|
+
|
15
|
+
OptionParser.new do |opts|
|
16
|
+
opts.banner = 'Usage: ruby connect_tacview [options]'
|
17
|
+
|
18
|
+
opts.on('-h', '--host=host', 'Tacview server hostname / IP') do |v|
|
19
|
+
options[:host] = v
|
20
|
+
end
|
21
|
+
|
22
|
+
opts.on('-p', '--port=port', 'Tacview server port (Default: 42674)') do |v|
|
23
|
+
options[:port] = v
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.on('-a', '--password=password', 'Tacview server password') do |v|
|
27
|
+
options[:password] = v
|
28
|
+
end
|
29
|
+
|
30
|
+
opts.on('-c', '--client-name=client', 'Client name (Default:' \
|
31
|
+
'ruby_tacview_client') do |v|
|
32
|
+
options[:client_name] = v
|
33
|
+
end
|
34
|
+
end.parse!
|
35
|
+
|
36
|
+
if !options[:host] || !options[:host].is_a?(String) || options[:host].empty?
|
37
|
+
puts 'Hostname required'
|
38
|
+
exit(1)
|
39
|
+
end
|
40
|
+
|
41
|
+
if options[:client_name].empty?
|
42
|
+
puts 'client-name cannot be empty'
|
43
|
+
exit(1)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Add console logging for this command-line app
|
47
|
+
module TacviewClientLogger
|
48
|
+
def initialize(host:,
|
49
|
+
port: 42_674,
|
50
|
+
password: nil,
|
51
|
+
processor:,
|
52
|
+
client_name: 'ruby_tacview_client')
|
53
|
+
|
54
|
+
print "Connecting to #{host}:#{port} as #{client_name}"
|
55
|
+
puts password ? ' with a password supplied' : ''
|
56
|
+
|
57
|
+
super(host: host, port: port, password: password, processor: processor,
|
58
|
+
client_name: client_name)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def read_handshake
|
64
|
+
puts 'Reading Incoming Handshake'
|
65
|
+
super
|
66
|
+
end
|
67
|
+
|
68
|
+
def read_handshake_header(header)
|
69
|
+
print header.to_s.tr('_', ' ').capitalize + ': '
|
70
|
+
result = super
|
71
|
+
puts result
|
72
|
+
result
|
73
|
+
end
|
74
|
+
|
75
|
+
def send_handshake
|
76
|
+
puts 'Sending Handshake'
|
77
|
+
super
|
78
|
+
end
|
79
|
+
|
80
|
+
def start_reader
|
81
|
+
puts 'Starting event stream reader'
|
82
|
+
puts '-' * 80
|
83
|
+
super
|
84
|
+
end
|
85
|
+
end
|
86
|
+
# A very simple processor that outputs all events received from a
|
87
|
+
# TacviewClient::Reader instance to the console. Useful for simple
|
88
|
+
# testing apps
|
89
|
+
class ConsoleOutputter < TacviewClient::BaseProcessor
|
90
|
+
def update_object(object)
|
91
|
+
puts "Object Update : #{object}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def delete_object(object_id)
|
95
|
+
puts "Object Deletion: #{object_id}"
|
96
|
+
end
|
97
|
+
|
98
|
+
def update_time(time)
|
99
|
+
puts "Time Update : #{time}"
|
100
|
+
end
|
101
|
+
|
102
|
+
def set_property(property:, value:)
|
103
|
+
puts "Property Set : #{property}, #{value}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
TacviewClient::Client.prepend TacviewClientLogger
|
108
|
+
|
109
|
+
client = TacviewClient::Client.new host: options[:host],
|
110
|
+
port: options[:port],
|
111
|
+
password: options[:password],
|
112
|
+
client_name: options[:client_name],
|
113
|
+
processor: ConsoleOutputter.new
|
114
|
+
client.connect
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TacviewClient
|
4
|
+
# An Base Procesor that defines all the methods called by an instance of
|
5
|
+
# a TacviewClient::Reader. Use this as a guide on what you need to
|
6
|
+
# implement. You can also optionally inherit from this class to make
|
7
|
+
# sure you have every method defined in your sub-class
|
8
|
+
class BaseProcessor
|
9
|
+
# Process an update event for an object
|
10
|
+
#
|
11
|
+
# On the first appearance of the object there are usually more fields
|
12
|
+
# including pilot names, object type etc.
|
13
|
+
#
|
14
|
+
# For existing objects these events are almost always lat/lon/alt updates
|
15
|
+
# only
|
16
|
+
#
|
17
|
+
# @param event [Hash] A parsed ACMI line. This hash will always include
|
18
|
+
# the fields listed below but may also include others depending on the
|
19
|
+
# source line.
|
20
|
+
# @option event [String] :object_id The hexadecimal format object ID.
|
21
|
+
# @option event [BigDecimal] :latitude The object latitude in WGS 84 format.
|
22
|
+
# @option event [BigDecimal] :longitude The object latitude in WGS 84
|
23
|
+
# format.
|
24
|
+
# @option event [BigDecimal] :altitude The object altitude above sea level
|
25
|
+
# in meters to 1 decimal place.
|
26
|
+
def update_object(event)
|
27
|
+
raise NotImplementedError, 'To be implemented by subclass'
|
28
|
+
end
|
29
|
+
|
30
|
+
# Process a delete event for an object
|
31
|
+
#
|
32
|
+
# @param object_id [String] A hexadecimal number representing the object
|
33
|
+
# ID
|
34
|
+
def delete_object(object_id)
|
35
|
+
raise NotImplementedError, 'To be implemented by subclass'
|
36
|
+
end
|
37
|
+
|
38
|
+
# Process a time update event
|
39
|
+
#
|
40
|
+
# @param time [BigDecimal] A time update in seconds from the
|
41
|
+
# ReferenceTime to 2 decimal places
|
42
|
+
def update_time(time)
|
43
|
+
raise NotImplementedError, 'To be implemented by subclass'
|
44
|
+
end
|
45
|
+
|
46
|
+
# Set a property
|
47
|
+
#
|
48
|
+
# @param property [String] The name of the property to be set
|
49
|
+
# @param value [String] The value of the property to be set
|
50
|
+
def set_property(property:, value:)
|
51
|
+
raise NotImplementedError, 'To be implemented by subclass'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
require 'crc'
|
5
|
+
|
6
|
+
require_relative 'reader'
|
7
|
+
|
8
|
+
module TacviewClient
|
9
|
+
# The actual client to be instantiated to connect to a Tacview Server
|
10
|
+
class Client
|
11
|
+
# The underlying stream protocol used by Tacview. Needs to be in-sync
|
12
|
+
# between the client and the server.
|
13
|
+
STREAM_PROTOCOL = 'XtraLib.Stream.0'
|
14
|
+
|
15
|
+
# The application level protocol used by Tacview. Needs to be in-sync
|
16
|
+
# between the client and the server.
|
17
|
+
TACVIEW_PROTOCOL = 'Tacview.RealTimeTelemetry.0'
|
18
|
+
|
19
|
+
# A null terminator used by Tacview to terminate handshake packages
|
20
|
+
HANDSHAKE_TERMINATOR = "\0"
|
21
|
+
|
22
|
+
# Passwords sent between Tacview clients and servers are hashed using
|
23
|
+
# this algorithm
|
24
|
+
PASSWORD_HASHER = CRC['CRC-64-ECMA']
|
25
|
+
|
26
|
+
# Returns a new instance of a Client
|
27
|
+
#
|
28
|
+
# This is the entry point into the gem. Instantiate an instance of this
|
29
|
+
# class to setup the prerequisite data for a connection to a Tacview server.
|
30
|
+
# Once done call {#connect} to start processing the Tacview ACMI stream.
|
31
|
+
#
|
32
|
+
# @param host [String] Server hostname or IP
|
33
|
+
# @param port [Integer] Server port
|
34
|
+
# @param password [String] Plaintext password required to connect to a
|
35
|
+
# password protected Tacview server. Is hashed before transmission.
|
36
|
+
# @param client_name [String] Client name to send to the server
|
37
|
+
# @param processor [BaseProcessor] The object that processes the events
|
38
|
+
# emitted by the {Reader}. Must implement the methods defined by the
|
39
|
+
# {BaseProcessor} and can optionally inherit from it.
|
40
|
+
def initialize(host:,
|
41
|
+
port: 42_674,
|
42
|
+
password: nil,
|
43
|
+
processor:,
|
44
|
+
client_name: 'ruby_tacview_client')
|
45
|
+
@host = host
|
46
|
+
@port = port
|
47
|
+
@password = password
|
48
|
+
@processor = processor
|
49
|
+
@client_name = client_name
|
50
|
+
end
|
51
|
+
|
52
|
+
# Connect to the Tacview server
|
53
|
+
#
|
54
|
+
# Actually opens a TCP connection to the Tacview server and starts
|
55
|
+
# streaming ACMI lines to an instance of the {Reader} class.
|
56
|
+
#
|
57
|
+
# This method will only return when the TCP connection has be killed
|
58
|
+
# either by a client-side signal or by the server closing the TCP
|
59
|
+
# connection.
|
60
|
+
def connect
|
61
|
+
@connection = TCPSocket.open(@host, @port)
|
62
|
+
|
63
|
+
read_handshake
|
64
|
+
|
65
|
+
send_handshake
|
66
|
+
|
67
|
+
start_reader
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# See https://www.tacview.net/documentation/realtime/en/
|
73
|
+
# for information on connection negotiation
|
74
|
+
def read_handshake
|
75
|
+
stream_protocol = read_handshake_header :stream_protocol
|
76
|
+
validate_handshake_header STREAM_PROTOCOL, stream_protocol
|
77
|
+
|
78
|
+
tacview_protocol = read_handshake_header :tacview_protocol
|
79
|
+
validate_handshake_header TACVIEW_PROTOCOL, tacview_protocol
|
80
|
+
|
81
|
+
read_handshake_header :host
|
82
|
+
|
83
|
+
@connection.gets HANDSHAKE_TERMINATOR
|
84
|
+
end
|
85
|
+
|
86
|
+
# Header parameter included for logging purposes
|
87
|
+
def read_handshake_header(_header)
|
88
|
+
@connection.gets.chomp
|
89
|
+
end
|
90
|
+
|
91
|
+
def validate_handshake_header(expected, actual)
|
92
|
+
return if expected == actual
|
93
|
+
|
94
|
+
abort_connection
|
95
|
+
end
|
96
|
+
|
97
|
+
def abort_connection
|
98
|
+
@connection.close
|
99
|
+
exit(1)
|
100
|
+
end
|
101
|
+
|
102
|
+
def send_handshake
|
103
|
+
@connection.print [
|
104
|
+
STREAM_PROTOCOL,
|
105
|
+
TACVIEW_PROTOCOL,
|
106
|
+
@client_name,
|
107
|
+
hash_password
|
108
|
+
].join("\n") + HANDSHAKE_TERMINATOR
|
109
|
+
end
|
110
|
+
|
111
|
+
def hash_password
|
112
|
+
return 0 unless @password
|
113
|
+
|
114
|
+
PASSWORD_HASHER.crc(@password)
|
115
|
+
end
|
116
|
+
|
117
|
+
def start_reader
|
118
|
+
reader = Reader.new(input_source: @connection,
|
119
|
+
processor: @processor)
|
120
|
+
reader.start_reading
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bigdecimal'
|
4
|
+
require_relative './reader/parser'
|
5
|
+
|
6
|
+
module TacviewClient
|
7
|
+
# Reads events from an input source, parses them using the {Parser}
|
8
|
+
# and calls the appropriate event processor method
|
9
|
+
class Reader
|
10
|
+
# The format that matches the beginning of an ACMI update line
|
11
|
+
OBJECT_UPDATE_MARKER = '^\h\h+,'
|
12
|
+
|
13
|
+
# The format that matches the beginning of an ACMI delete line
|
14
|
+
OBJECT_DELETION_MARKER = '-'
|
15
|
+
|
16
|
+
# The format that matches the beginning of an ACMI time update line
|
17
|
+
TIME_UPDATE_MARKER = '#'
|
18
|
+
|
19
|
+
# The format that matches the beginning of an ACMI Property line
|
20
|
+
GLOBAL_PROPERTY_MARKER = '0,'
|
21
|
+
|
22
|
+
# @param input_source [IO, #gets] An {IO} object (or object that implements
|
23
|
+
# the {IO#gets} method. Typically this is a Socket or File.
|
24
|
+
# @param processor [BaseProcessor] The object that processes the events
|
25
|
+
# emitted by the {Reader}. Must implement the methods defined by the
|
26
|
+
# {BaseProcessor} and can optionally inherit from it.
|
27
|
+
def initialize(input_source:, processor:)
|
28
|
+
raise ArgumentError, 'input_source cannot be nil' if input_source.nil?
|
29
|
+
raise ArgumentError, 'processor cannot be nil' if processor.nil?
|
30
|
+
|
31
|
+
@input_source = input_source
|
32
|
+
@processor = processor
|
33
|
+
end
|
34
|
+
|
35
|
+
def start_reading
|
36
|
+
while (line = @input_source.gets)
|
37
|
+
route_line(line.chomp)
|
38
|
+
end
|
39
|
+
true
|
40
|
+
rescue SignalException
|
41
|
+
exit
|
42
|
+
ensure
|
43
|
+
@input_source.close
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def route_line(line)
|
49
|
+
if line.match?(OBJECT_UPDATE_MARKER)
|
50
|
+
object_update(line)
|
51
|
+
elsif line[0] == TIME_UPDATE_MARKER
|
52
|
+
time_update(line)
|
53
|
+
elsif line[0] == OBJECT_DELETION_MARKER
|
54
|
+
object_deletion(line)
|
55
|
+
elsif line[0..1] == GLOBAL_PROPERTY_MARKER
|
56
|
+
global_property(line)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def object_update(line)
|
61
|
+
result = Parser.new.parse_object_update(line)
|
62
|
+
@processor.update_object(result) if result
|
63
|
+
end
|
64
|
+
|
65
|
+
def object_deletion(line)
|
66
|
+
@processor.delete_object line[1..-1]
|
67
|
+
end
|
68
|
+
|
69
|
+
def time_update(line)
|
70
|
+
@processor.update_time BigDecimal(line[1..-1])
|
71
|
+
end
|
72
|
+
|
73
|
+
def global_property(line)
|
74
|
+
key, value = line[2..-1].split('=')
|
75
|
+
|
76
|
+
if value.end_with?('\\')
|
77
|
+
value = [value] + read_multiline
|
78
|
+
value = value.inject([]) do |arr, array_line|
|
79
|
+
arr << array_line.delete('\\').strip
|
80
|
+
end.join("\n")
|
81
|
+
end
|
82
|
+
|
83
|
+
@processor.set_property(property: key, value: value.strip)
|
84
|
+
end
|
85
|
+
|
86
|
+
def read_multiline
|
87
|
+
array_lines = []
|
88
|
+
while (line = @input_source.readline)
|
89
|
+
array_lines << line
|
90
|
+
break unless line.end_with?('\\')
|
91
|
+
end
|
92
|
+
|
93
|
+
array_lines
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'strscan'
|
4
|
+
|
5
|
+
module TacviewClient
|
6
|
+
class Reader
|
7
|
+
# Parses an event coming out of the Tacview server when they are
|
8
|
+
# too complicated for simple extraction.
|
9
|
+
#
|
10
|
+
# Not to be used directly by library users, should only be called
|
11
|
+
# by an instance of the Reader class
|
12
|
+
#
|
13
|
+
# See https://www.tacview.net/documentation/acmi/en/ for information
|
14
|
+
# of the Tacview ACMI format this module is parsing
|
15
|
+
# @private
|
16
|
+
class Parser
|
17
|
+
HEX_NUMBER = /\h+/.freeze
|
18
|
+
OPTIONALLY_DECIMAL_NUMBER = /\d+\.?\d+/.freeze
|
19
|
+
POSITION_START_INDICATOR = /T=/.freeze
|
20
|
+
POSITION_SEPARATOR = /\|/.freeze
|
21
|
+
FIELD_SEPARATOR = /,/.freeze
|
22
|
+
END_OF_FILE = /$/.freeze
|
23
|
+
END_OF_FIELD = Regexp.union(FIELD_SEPARATOR, END_OF_FILE)
|
24
|
+
|
25
|
+
# Parse an ACMI line associated with the update of an object.
|
26
|
+
def parse_object_update(line)
|
27
|
+
@scanner = StringScanner.new(line)
|
28
|
+
@result = {}
|
29
|
+
|
30
|
+
parse_object_id
|
31
|
+
|
32
|
+
return nil if own_vehicle_message?
|
33
|
+
|
34
|
+
parse_lon_lat_alt
|
35
|
+
parse_heading
|
36
|
+
parse_fields
|
37
|
+
|
38
|
+
@result
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def parse_object_id
|
44
|
+
@result[:object_id] = @scanner.scan(HEX_NUMBER)
|
45
|
+
@scanner.skip FIELD_SEPARATOR
|
46
|
+
end
|
47
|
+
|
48
|
+
# Updates without a position are always from own vehicle based on looking
|
49
|
+
# through samples. We don't care about these so ignore them
|
50
|
+
def own_vehicle_message?
|
51
|
+
@scanner.peek(2) != POSITION_START_INDICATOR.source
|
52
|
+
end
|
53
|
+
|
54
|
+
def parse_lon_lat_alt
|
55
|
+
@scanner.skip POSITION_START_INDICATOR
|
56
|
+
|
57
|
+
%i[longitude latitude altitude].each do |field|
|
58
|
+
value = @scanner.scan(OPTIONALLY_DECIMAL_NUMBER)
|
59
|
+
@result[field] = BigDecimal(value) if value
|
60
|
+
@scanner.skip POSITION_SEPARATOR
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse_heading
|
65
|
+
return if end_of_message?
|
66
|
+
|
67
|
+
# Check to see if the heading (9th field which is 4 more
|
68
|
+
# separators from our current position) is present
|
69
|
+
if @scanner.check_until(END_OF_FIELD)
|
70
|
+
.count(POSITION_SEPARATOR.source) == 4
|
71
|
+
# If it is then we will save that as well by skipping all the
|
72
|
+
# text until the heading value
|
73
|
+
@scanner.scan_until(/\|[\|\-?0-9.]*\|/)
|
74
|
+
heading = @scanner.scan OPTIONALLY_DECIMAL_NUMBER
|
75
|
+
@result[:heading] = BigDecimal(heading) if heading
|
76
|
+
end
|
77
|
+
|
78
|
+
@scanner.scan_until END_OF_FIELD
|
79
|
+
end
|
80
|
+
|
81
|
+
def parse_fields
|
82
|
+
until end_of_message?
|
83
|
+
field = @scanner.scan_until END_OF_FIELD
|
84
|
+
field.chomp! FIELD_SEPARATOR.source
|
85
|
+
|
86
|
+
key, value = field.split('=', 2)
|
87
|
+
@result[key.downcase.to_sym] = value
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def end_of_message?
|
92
|
+
@scanner.eos?
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'tacview_client/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'tacview_client'
|
9
|
+
spec.version = TacviewClient::VERSION
|
10
|
+
spec.authors = ['Jeffrey Jones']
|
11
|
+
spec.email = ['jeff@jones.be']
|
12
|
+
|
13
|
+
spec.summary = 'Tacview client written in ruby'
|
14
|
+
spec.description = 'Tacview client written in ruby'
|
15
|
+
spec.homepage = 'https://gitlab.com/overlord-bot/tacview-ruby-client'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
19
|
+
f.match(%r{^(test|spec|features)/})
|
20
|
+
end
|
21
|
+
spec.bindir = 'exe'
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
+
spec.require_paths = ['lib']
|
24
|
+
|
25
|
+
spec.metadata['yard.run'] = 'yri'
|
26
|
+
|
27
|
+
spec.add_dependency 'crc', '~> 0.4'
|
28
|
+
|
29
|
+
spec.add_development_dependency 'bundler', '~> 2.0'
|
30
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
31
|
+
spec.add_development_dependency 'rspec', '~> 3.8'
|
32
|
+
spec.add_development_dependency 'rubocop', '~>0.73'
|
33
|
+
spec.add_development_dependency 'simplecov', '~>0.17'
|
34
|
+
spec.add_development_dependency 'yard', '~>0.9'
|
35
|
+
end
|
metadata
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tacview_client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jeffrey Jones
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-09-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: crc
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.4'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.4'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.8'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.8'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.73'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.73'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: simplecov
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.17'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.17'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: yard
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.9'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.9'
|
111
|
+
description: Tacview client written in ruby
|
112
|
+
email:
|
113
|
+
- jeff@jones.be
|
114
|
+
executables:
|
115
|
+
- connect_tacview
|
116
|
+
extensions: []
|
117
|
+
extra_rdoc_files: []
|
118
|
+
files:
|
119
|
+
- ".gitignore"
|
120
|
+
- ".gitlab-ci.yml"
|
121
|
+
- ".jrubyrc"
|
122
|
+
- ".rspec"
|
123
|
+
- ".rubocop.yml"
|
124
|
+
- ".ruby-gemset"
|
125
|
+
- ".ruby-version"
|
126
|
+
- Gemfile
|
127
|
+
- LICENSE.txt
|
128
|
+
- README.md
|
129
|
+
- Rakefile
|
130
|
+
- bin/console
|
131
|
+
- bin/setup
|
132
|
+
- exe/connect_tacview
|
133
|
+
- lib/tacview_client.rb
|
134
|
+
- lib/tacview_client/base_processor.rb
|
135
|
+
- lib/tacview_client/client.rb
|
136
|
+
- lib/tacview_client/reader.rb
|
137
|
+
- lib/tacview_client/reader/parser.rb
|
138
|
+
- lib/tacview_client/version.rb
|
139
|
+
- tacview_client.gemspec
|
140
|
+
homepage: https://gitlab.com/overlord-bot/tacview-ruby-client
|
141
|
+
licenses:
|
142
|
+
- MIT
|
143
|
+
metadata:
|
144
|
+
yard.run: yri
|
145
|
+
post_install_message:
|
146
|
+
rdoc_options: []
|
147
|
+
require_paths:
|
148
|
+
- lib
|
149
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
150
|
+
requirements:
|
151
|
+
- - ">="
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0'
|
154
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - ">="
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
requirements: []
|
160
|
+
rubygems_version: 3.0.3
|
161
|
+
signing_key:
|
162
|
+
specification_version: 4
|
163
|
+
summary: Tacview client written in ruby
|
164
|
+
test_files: []
|