Ravn::

BDE module

The Ravn Battlefield Decision Engine, the core of Helios.

SBIR DATA RIGHTS

Contract No. FA8649-19-9-9031 Contractor Name: Ravn Inc. Contractor Address: 548 Market Street, PMB 80382, San Francisco, CA 94104, United States Expiration of SBIR Data Rights Period: 7 August 2039

The Government’s rights to use, modify, reproduce, release, perform, display, or disclose technical data or computer software marked with this legend are restricted during the period shown as provided in paragraph (b)(4) of the Rights in Noncommercial Technical Data and Computer Software—Small Business Innovation Research (SBIR) Program clause contained in the above identified contract. No restrictions apply after the expiration date shown above. Any reproduction of technical data, computer software, or portions thereof marked with this legend must also reproduce the markings.

Constants

DEFAULT_MISSION_FILE

The default types of bolts to load

MISSION_FORMAT_VERSION

Format version for tracking what version of this code was running when logs were written, as well as what features are supported in mission config files.

You should change this if you’re adding mandatory fields to the mission config.

RAVN_CALLSIGN_UPPER_BOUNDS

The maximum number in a dynamically-generated callsign

REQUIRED_MISSION_CONFIG_KEYS

The keys that are required to be present in a valid mission config

REQUIRED_POI_FIELDS

The keys that are required to be present in a valid POI

VALID_CALLSIGN

Regular expression that matches a valid callsign. :todo: Figure out if this can be reasonably stricter. E.g., 50 chars is not going to look good in the small UIs.

VALID_DEVICE_ID

Regular expression that matches a valid device ID

VALID_LATITUDE

Range of valid latitudes

VALID_LONGITUDE

Range of valid longitudes

VERSION

Package version

Public Class Methods

callsign()

Return the callsign assigned to the local host in the specified mission_config. If the mission_config has no callsign assignments, assign a dynamic one based on the host’s device ID. If the host’s device ID is not listed in the (non-empty) callsigns mappting, raise an error.

# File lib/ravn/bde.rb, line 170
def self::callsign
        return @callsign ||= begin
                callsigns = self.mission[ :callsigns ]

                if callsigns.nil? || callsigns.empty?
                        self.get_hostbased_callsign
                else
                        id = Ravn.device_id
                        callsigns = callsigns.transform_keys {|k| k.to_s.downcase }

                        # Support both lowercased hex and legacy decimal keys
                        callsigns[ id ] || callsigns[ id.to_i(16).to_s ] or
                                raise Ravn::BDE::ValidationError, "no callsign for device %p" % [ id ]
                end
        end
end
callsigns()

Return an Array of all callsigns in the current mission config. Returns an Array of only your callsign if no callsigns are configured.

# File lib/ravn/bde.rb, line 190
def self::callsigns
        return @callsigns ||= begin
                callsigns_map = self.mission[ :callsigns ]
                self.log.debug "Making callsigns array from config: %p" % [ callsigns_map ]

                if callsigns_map.nil? || callsigns_map.empty?
                        [ self.get_hostbased_callsign ]
                else
                        callsigns_map.values
                end
        end
end
data_dir()

The Pathname of the gem’s data directory

# File lib/ravn/bde.rb, line 113
singleton_attr_accessor :data_dir
default_mission_config_path()

Return a Pathname to the default (fallback) mission config file.

# File lib/ravn/bde.rb, line 265
def self::default_mission_config_path
        return self.data_dir + DEFAULT_MISSION_FILE
end
get_hostbased_callsign()

Return a callsign based on the host device ID.

# File lib/ravn/bde.rb, line 230
def self::get_hostbased_callsign
        id = Ravn.device_id.to_i( 16 )
        return "RAVN-%s" % [ id % RAVN_CALLSIGN_UPPER_BOUNDS ]
end
handle_QUIT()

Ravn::ActorRunner API – hook the QUIT signal. Cleanly shut down all actors and wipe databases.

# File lib/ravn/bde.rb, line 474
def self::handle_QUIT
        self.controller&.ask!( :terminate! )
        Ravn::BDE::StateManager.reset
        Ravn::BDE::MissionRecord.reset
        self.exitcode = :tempfail
end
history_db_path()

Return a Pathname to the history database.

# File lib/ravn/bde.rb, line 253
def self::history_db_path
        return self.mission_directory + self.history_db
end
load_mission_config()

Load the mission config file if it exists, or fall back to a simple default config if it doesn’t.

# File lib/ravn/bde.rb, line 272
def self::load_mission_config
        self.log.info "Loading mission config: %s" % [ self.mission_config_path ]
        file = self.mission_config_path

        unless file.exist?
                file = self.default_mission_config_path
                self.log.warn "No mission config; loading default mission from %s" % [ file ]
        end

        return self.load_mission_config_file( file )
end
load_mission_config_file( file )

Load the specified file as a YAML mission config, validate it, and return it.

# File lib/ravn/bde.rb, line 299
def self::load_mission_config_file( file )
        hash = self.read_mission_config_file( file )
        self.validate_mission_config( hash )

        return hash
end
mission()

Return the current mission config as a Hash, loading it if necessary.

# File lib/ravn/bde.rb, line 161
def self::mission
        return @mission ||= self.load_mission_config
end
mission_config_path()

Return a Pathname to the mission config file.

# File lib/ravn/bde.rb, line 259
def self::mission_config_path
        return self.mission_directory + self.mission_config_filename
end
read_mission_config_file( file )

Load the unvalidated mission config hash from file and return it.

# File lib/ravn/bde.rb, line 286
def self::read_mission_config_file( file )
        file = Pathname( file )
        hash = Psych.safe_load_file( file, symbolize_names: true )

        hash[:timestamp] = file.mtime
        hash[:bolts] = hash[:bolts].transform_keys( &:to_s )
        hash[:segments] = hash[:segments].transform_keys( &:to_s )

        return hash
end
reset()

Reset instance variables and databases that should be reset before a new run.

# File lib/ravn/bde.rb, line 237
def self::reset
        @mission    = nil
        @controller = nil
        @callsign   = nil
        @callsigns  = nil
        @segments   = nil
end
segments()

Return a Hash of Segments specified by the mission config.

# File lib/ravn/bde.rb, line 205
def self::segments
        return @segments ||= begin
                segments = self.mission[ :segments ]

                segments = segments.each_with_object( {} ) do |(name, conf), acc|
                        name = name.to_s

                        type = conf.delete( :type ) or raise Ravn::BDE::ValidationError,
                                "Missing 'type' key for segment: %p" % [ name ]
                        conf = conf.delete( :config ) or raise Ravn::BDE::ValidationError,
                                "Missing 'config' key for segment: %p" % [ name ]

                        self.log.debug "Creating a segment of type %p: %p" % [ type, conf ]
                        acc[ name ] = Ravn::BDE::Segment.create( type, name, **conf )
                end

                default_segment = Ravn::BDE::Segment.default
                segments[ default_segment.name ] = default_segment

                segments
        end
end
spawn_toplevel_actor( *args, **options )

Ravn::ActorRunner API – Run the top level actor of the BDE.

# File lib/ravn/bde.rb, line 464
def self::spawn_toplevel_actor( *args, **options )
        raise "already spawned the controller" if @controller

        @controller = Ravn::BDE::MissionController.spawn( :mission_controller )
        return @controller
end
state_db_path()

Return a Pathname to the state database.

# File lib/ravn/bde.rb, line 247
def self::state_db_path
        return self.mission_directory + self.state_db
end
validate_bolts( config )

Ensure that the bolts in the given config are valid, raising a Ravn::BDE::ValidationError if not.

# File lib/ravn/bde.rb, line 459
def self::validate_bolts( config )
end
validate_callsigns( config )

Ensure that the callsigns section in the given config is valid, raising a Ravn::BDE::ValidationError if not.

# File lib/ravn/bde.rb, line 364
def self::validate_callsigns( config )
        callsigns = config[ :callsigns ] or return # nil callsigns is valid
        callsigns ||= {}

        unless callsigns.is_a?( Hash )
                raise Ravn::BDE::ValidationError,
                        "invalid callsigns map: expected nil or a Hash, but was %p" % [ callsigns.class ]
        end

        callsigns.keys.each do |device_id|
                unless device_id.to_s.match?( VALID_DEVICE_ID )
                        raise Ravn::BDE::ValidationError,
                                "invalid device ID in callsigns map: %p" % [ device_id ]
                end
        end

        callsigns.each do |device_id, callsign|
                if callsigns.key( callsign ) != device_id
                        raise Ravn::BDE::ValidationError,
                                "duplicate callsign in callsigns map: %s" % [ callsign ]
                elsif !callsign.match?( VALID_CALLSIGN )
                        raise Ravn::BDE::ValidationError,
                                "invalid callsign in callsigns map: %p" % [ callsign ]
                end
        end
end
validate_mission_config( config )

Raises an exception if the specified config is not a valid mission config.

# File lib/ravn/bde.rb, line 308
def self::validate_mission_config( config )
        id = config[:id] or
                raise Ravn::BDE::ValidationError, "mission config missing `id` field"

        self.log.info "Validating mission config %s" % [ id ]
        raise Ravn::BDE::ValidationError, "Mission config is not a Hash" unless config.is_a?( Hash )

        self.validate_mission_format_version( config )
        self.validate_mission_keys( config )
        self.validate_spoofed_gps_coordinates( config )
        self.validate_callsigns( config )
        self.validate_pois( config )
        self.validate_segments( config )
        self.validate_bolts( config )
end
validate_mission_format_version( config )

Ensure that the format_version of the given mission_config is supported, raising a Ravn::BDE::ValidationError if not.

# File lib/ravn/bde.rb, line 327
def self::validate_mission_format_version( config )
        format_version = config[:format_version] || 0
        if format_version < Ravn::BDE::MISSION_FORMAT_VERSION
                raise Ravn::BDE::ValidationError,
                        "Incompatible mission config; expected version >= %d, got %d" %
                                [ Ravn::BDE::MISSION_FORMAT_VERSION, format_version ]
        end
end
validate_mission_keys( config )

Ensure that the given config contains all the required mission config keys, raising a Ravn::BDE::ValidationError if not.

# File lib/ravn/bde.rb, line 339
def self::validate_mission_keys( config )
        missing = REQUIRED_MISSION_CONFIG_KEYS.dup - config.keys
        unless missing.empty?
                missing = missing.sort.join( ', ' )
                raise Ravn::BDE::ValidationError,
                        "Mission config missing required sections: %s" % [ missing ]
        end
end
validate_pois( config )

Ensure that any configured Point of Interests in the given config are valid, raising a Ravn::BDE::ValidationError if not.

# File lib/ravn/bde.rb, line 394
def self::validate_pois( config )
        pois = config[ :pois ] or return # nil pois is valid

        unless pois.is_a?( Array )
                raise Ravn::BDE::ValidationError,
                        "invalid pois section: expected nil or an Array, but was %p" % [ pois.class ]
        end

        counts = pois.each_with_object( Hash.new(0) ).with_index do |(poi, acc), idx|
                missing = REQUIRED_POI_FIELDS - poi.keys
                unless missing.empty?
                        raise Ravn::BDE::ValidationError,
                                "invalid poi at index %d: missing required key(s): \"%s\"" % [
                                        idx,
                                        missing.join( ', ' )
                                ]
                end

                unless VALID_LATITUDE.include?( poi[:latitude] )
                        raise Ravn::BDE::ValidationError,
                                "invalid pois section: invalid latitude: %p" % [ poi[:latitude] ]
                end

                unless VALID_LONGITUDE.include?( poi[:longitude] )
                        raise Ravn::BDE::ValidationError,
                                "invalid pois section: invalid longitude: %p" % [ poi[:longitude] ]
                end

                acc[ poi[ :name ] ] += 1
        end

        duplicates = counts.select{|name, count| count > 1}.keys
        unless duplicates.empty?
                raise Ravn::BDE::ValidationError, "invalid pois section: duplicate name(s): \"%s\"" % [
                        duplicates.join( ', ' )
                ]
        end
end
validate_segments( config )

Ensure that the segments section in the given config is valid, raising a Ravn::BDE::ValidationError if not.

# File lib/ravn/bde.rb, line 436
def self::validate_segments( config )
        segments = config[ :segments ]

        segments.each do |name, options|
                type = options[:type] or raise Ravn::BDE::ValidationError,
                        "segment '%s' missing required field `type'" % [ name ]

                begin
                        segment_type = Ravn::BDE::Segment.get_subclass( type )
                        segment_type.validate( options[:config], config )
                rescue Ravn::BDE::ValidationError => err
                        raise err, "`%s' segment: %s" % [ name.to_s, err.message ]
                rescue => err
                        raise Ravn::BDE::ValidationError,
                                "problem validating `%s' segment named `%s'" % [ type, name ],
                                cause: err
                end
        end
end
validate_spoofed_gps_coordinates( config )

If the given config has spoofed GPS coordinates, ensure that they’re in the correct format and are valid values. Raises a Ravn::BDE::ValidationError if not.

# File lib/ravn/bde.rb, line 351
def self::validate_spoofed_gps_coordinates( config )
        if ( coordinates = config[:spoof_gps_coordinates] )
                if (( coordinates[0].nil? || coordinates[1].nil? ) ||
                                ( not( coordinates[0].is_a? Float ) || not( coordinates[1].is_a? Float ) )
                   )
                        raise Ravn::BDE::ValidationError, "malformed mission config, spoof_gps_coordinates requires lat and lon to be array with two floating point numbers"
                end
        end
end

Public Instance Methods

controller()

The reference to the running MissionController actor if it’s been started.

# File lib/ravn/bde.rb, line 157
singleton_attr_reader :controller