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.
- 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
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.
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 }
callsigns[ id ] || callsigns[ id.to_i(16).to_s ] or
raise Ravn::BDE::ValidationError, "no callsign for device %p" % [ id ]
end
end
end
Return an Array of all callsigns in the current mission config. Returns an Array of only your callsign if no callsigns are configured.
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
The Pathname of the gem’s data directory
singleton_attr_accessor :data_dir
default_mission_config_path()
Return a Pathname to the default (fallback) mission config file.
def self::default_mission_config_path
return self.data_dir + DEFAULT_MISSION_FILE
end
Return a callsign based on the host device ID.
def self::get_hostbased_callsign
id = Ravn.device_id.to_i( 16 )
return "RAVN-%s" % [ id % RAVN_CALLSIGN_UPPER_BOUNDS ]
end
Ravn::ActorRunner API – hook the QUIT signal. Cleanly shut down all actors and wipe databases.
def self::handle_QUIT
self.controller&.ask!( :terminate! )
Ravn::BDE::StateManager.reset
Ravn::BDE::MissionRecord.reset
self.exitcode = :tempfail
end
Return a Pathname to the history database.
def self::history_db_path
return self.mission_directory + self.history_db
end
Load the mission config file if it exists, or fall back to a simple default config if it doesn’t.
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.
def self::load_mission_config_file( file )
hash = self.read_mission_config_file( file )
self.validate_mission_config( hash )
return hash
end
Return the current mission config as a Hash, loading it if necessary.
def self::mission
return @mission ||= self.load_mission_config
end
Return a Pathname to the mission config file.
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.
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 instance variables and databases that should be reset before a new run.
def self::reset
@mission = nil
@controller = nil
@callsign = nil
@callsigns = nil
@segments = nil
end
Return a Hash of Segments specified by the mission config.
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
.
def self::spawn_toplevel_actor( *args, **options )
raise "already spawned the controller" if @controller
@controller = Ravn::BDE::MissionController.spawn( :mission_controller )
return @controller
end
Return a Pathname to the state database.
def self::state_db_path
return self.mission_directory + self.state_db
end
Ensure that the bolts in the given config
are valid, raising a Ravn::BDE::ValidationError
if not.
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.
def self::validate_callsigns( config )
callsigns = config[ :callsigns ] or return
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.
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_keys( config )
Ensure that the given config
contains all the required mission config keys, raising a Ravn::BDE::ValidationError
if not.
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
Ensure that any configured Point of Interests in the given config
are valid, raising a Ravn::BDE::ValidationError
if not.
def self::validate_pois( config )
pois = config[ :pois ] or return
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.
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.
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
The reference to the running MissionController
actor if it’s been started.
singleton_attr_reader :controller