tacview_client 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.
- 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: []
|