# frozen_string_literal: true require 'socket' require 'crc' require_relative 'reader' module TacviewClient # The actual client to be instantiated to connect to a Tacview Server class Client # The underlying stream protocol used by Tacview. Needs to be in-sync # between the client and the server. STREAM_PROTOCOL = 'XtraLib.Stream.0' # The application level protocol used by Tacview. Needs to be in-sync # between the client and the server. TACVIEW_PROTOCOL = 'Tacview.RealTimeTelemetry.0' # A null terminator used by Tacview to terminate handshake packages HANDSHAKE_TERMINATOR = "\0" # Passwords sent between Tacview clients and servers are hashed using # this algorithm PASSWORD_HASHER = CRC['CRC-64-ECMA'] # Returns a new instance of a Client # # This is the entry point into the gem. Instantiate an instance of this # class to setup the prerequisite data for a connection to a Tacview server. # Once done call {#connect} to start processing the Tacview ACMI stream. # # @param host [String] Server hostname or IP # @param port [Integer] Server port # @param password [String] Plaintext password required to connect to a # password protected Tacview server. Is hashed before transmission. # @param client_name [String] Client name to send to the server # @param processor [BaseProcessor] The object that processes the events # emitted by the {Reader}. Must implement the methods defined by the # {BaseProcessor} and can optionally inherit from it. def initialize(host:, port: 42_674, password: nil, processor:, client_name: 'ruby_tacview_client') @host = host @port = port @password = password @processor = processor @client_name = client_name end # Connect to the Tacview server # # Actually opens a TCP connection to the Tacview server and starts # streaming ACMI lines to an instance of the {Reader} class. # # This method will only return when the TCP connection has be killed # either by a client-side signal or by the server closing the TCP # connection. def connect @connection = TCPSocket.open(@host, @port) read_handshake send_handshake start_reader end private # See https://www.tacview.net/documentation/realtime/en/ # for information on connection negotiation def read_handshake stream_protocol = read_handshake_header :stream_protocol validate_handshake_header STREAM_PROTOCOL, stream_protocol tacview_protocol = read_handshake_header :tacview_protocol validate_handshake_header TACVIEW_PROTOCOL, tacview_protocol read_handshake_header :host @connection.gets HANDSHAKE_TERMINATOR end # Header parameter included for logging purposes def read_handshake_header(_header) @connection.gets.chomp end def validate_handshake_header(expected, actual) return if expected == actual abort_connection end def abort_connection @connection.close exit(1) end def send_handshake @connection.print [ STREAM_PROTOCOL, TACVIEW_PROTOCOL, @client_name, hash_password ].join("\n") + HANDSHAKE_TERMINATOR end def hash_password return 0 unless @password PASSWORD_HASHER.crc(@password) end def start_reader reader = Reader.new(input_source: @connection, processor: @processor) reader.start_reading end end end