Ravn::HAL::Device::
PRC163Radio
class
L3Harris PRC-163 radio adapter.
This device reads and write asynchronously from a USB TTY device connected to the radio. It has a thread managing the low-level IO, and a timer to periodically send the commands that turn into the events required by the BDE (GPS, time, etc.).
Both reading and writing happen in two stages: a queue and a buffer.
write_queue
-> write_buffer
-> <TTY> -> read_buffer
-> read_queue
Commands for the radio are appended to the write_queue
. The IO thread pulls each command off of the queue and appends it to the write_buffer
, then writes as much as it can to the radio. It then reads any pending output from the radio into the read_buffer
and then breaks as many whole lines off of that as it can, appending each one to the read_queue
. The read_queue
is then scanned for events, and those are emitted up the tree.
== Emits:
-
sys.device.info
-
sys.gps.position
-
sys.gps.time
-
sys.radio.battery
-
sys.radio.channel
-
sys.radio.volume
-
sys.radio.versions
== Consumes:
This class includes code from the ruby-termios examples, used under the terms of the Ruby License. The original software does not include a copyright statement, so none is duplicated here.
Refs:
- AUTOVIVIFY
Auto-vivifying Hash Proc; used to create Hashes that auto-expand their contents
- COMMAND_PROMPT
Prompt text that should be stripped from radio output
- ECHOED_COMMANDS
An output line containing an echoed command
- ECHOED_COMMAND_LINE
- EOL
End-of-line character
- IO_SELECT_TIMEOUT
Maximum number of floating-point seconds to wait in the IO loop. This is effectively the maximum amount of time a command will spend in the write_queue
before being sent.
- IO_THREAD_WAIT_TIME
How long to wait for the IO thread to die
- L3H_DMS_FORMAT
Pattern for matching L3H’s weird degrees/minutes/seconds format.
- LINE_SEPARATOR
Character/s used to split radio output into lines
- LIST_ENDING_MARKER
Marker of the end of multi-line output.
- MAX_READ_SIZE
Maximum of bytes to read from the radio at one time
- OOB_MESSAGE_LINE
Pattern to match ongoing (out-of-band) status message lines in radio output
- PRESET_CHANGE_COMPLETE
Pattern that matches the OOB message when a preset is changed
- command_timer R
Concurrent::TimerTask that causes periodic information events to be emitted from the device.
- io_thread R
The Thread object that is reading and writing to/from the radio serial device
- radio R
The UART object connected via serial USB to the radio
- read_buffer R
A String buffer that contains output that has been read from the radio but not yet split into lines into the read_queue
- read_queue R
Line-oriented output from the radio that is waiting to be parsed
- write_buffer R
A String with unwritten commands for the radio. Commands are appended to this as they’re removed from the write_queue
by the IO thread.
- write_queue R
Commands for the radio that haven’t been written yet.
Create a new device adapter for a L3H PRC-163 radio.
def initialize( * )
@radio = nil
@io_thread = nil
@read_queue = []
@read_buffer = String.new
@write_queue = []
@write_buffer = String.new
@command_timer = self.make_command_timer or
raise "couldn't create the periodic command timer"
if self.class.simulation_mode?
self.log.warn "Adding simulation mode to %p" % [ self ]
self.extend( SimulationMode )
end
super
end
Append any data in the write_queue
to the write_buffer
.
def buffer_pending_writes
if (data = self.write_queue.shift)
self.write_buffer << data << LINE_SEPARATOR
end
end
build_gps_message( gps_info )
Construct and return a ‘sys.gps.position’ message using the given gps_info
.
def build_gps_message( gps_info )
return unless gps_info[ :state ] == :tracking
gps = {
pos: [ gps_info[ :latitude ], gps_info[ :longitude ] ],
hae: gps_info[ :altitude ],
ce: gps_info[ :epe ],
heading: gps_info[ :heading ],
velocity: gps_info[ :velocity ]
}
return Ravn::HAL::Message.new( 'sys.gps.position', data: gps )
end
build_gps_time_message( gps_info )
Construct and return a ‘sys.gps.time’ message using the given gps_info
.
def build_gps_time_message( gps_info )
return nil unless gps_info.key?( :timestamp )
raw_timestamp = gps_info[ :timestamp ]
return Ravn::HAL::Message.new( 'sys.gps.time', data: { time: raw_timestamp.to_i } )
end
Connect to the radio via serial UART.
def build_radio_connection
device = self.class.serial_device
speed = self.class.serial_speed
mode = self.class.serial_mode
self.log.info "Opening radio device: %p at %p(%p)" % [ device, speed, mode ]
uart = UART.open( device.to_s, speed, mode )
desc = self.dump_termios( uart )
self.log.info "Radio device terminal capabilities: %s" % [ desc ]
return uart
rescue SystemCallError => err
self.log.error "%p while opening the serial device: %s" % [ err.class, err.message ]
raise
end
Connect to the radio in the discovery phase
def discover
@radio = self.build_radio_connection
super
rescue => err
self.reset( "couldn't open the serial connection (%p)" % [ err.class ] )
end
emit_preset_change_complete_event( match_data )
Emit a sys.radio.channel event given match_data
for a OOB SYS_PRESETSTATUS line.
def emit_preset_change_complete_event( match_data )
transceiver = match_data[:transceiver][ /(\d+)/, 1 ] or
raise "no transceiver in match data: %p" % [ match_data ]
data = {
radio: self.class.object_id,
transceiver: transceiver.to_i,
name: match_data[:preset_name],
number: match_data[:preset_number].to_i,
waveform: match_data[:waveform],
}
message = Ravn::HAL::Message.new( 'sys.radio.channel', data: )
self.filter_up( message )
end
Return a Hash that contains information describing this device for intra-device communication; overridden to set the correct vendor
.
def gather_device_info
return super.merge( vendor: 'l3harris' )
end
handle_oob_status_line( line )
Handle any status lines emitted by the radio by emitting events for the stuff we care about.
def handle_oob_status_line( line )
case line
when PRESET_CHANGE_COMPLETE
self.emit_preset_change_complete_event( $~ )
else
self.log.debug "Unhandled OOB status line: %p" % [ line ]
end
end
Stop reading/writing timers while paused
def handle_paused_event( * )
self.log.info "Paused; resetting the command timer."
self.reset_command_timer
end
handle_resetting_event( * )
Stop cleanly when the device is going to be reset.
def handle_resetting_event( * )
self.log.warn "Resetting."
self.stop
end
handle_resumed_event( * )
Recreate and restart the reading/writing timers when resumed
def handle_resumed_event( * )
self.log.info "Paused; restarting the command timer."
self.command_timer.execute
end
handle_terminated_event( * )
Stop the device cleanly when it’s terminated.
def handle_terminated_event( * )
self.log.warn "Terminated."
self.stop
end
Connect to the radio and loop over the IO routine for it while it’s open.
def io_loop
Thread.current.report_on_exception = true
Thread.current.name = "PRC163 Device I/O"
self.log.info "Starting IO loop."
while self.radio && ! self.radio.closed?
self.read_and_write
end
self.log.info "Stopped IO loop."
rescue SystemCallError => err
self.log.error( err )
reason = "%p in the IO loop: %s" % [ err.class, err.message ]
self.reset( reason )
end
Turn data in the read_queue
into events if possible.
def parse_and_emit_events
line = self.read_queue.first or return
self.log.debug "Parsing and emitting events from the read queue: %p..." % [ self.read_queue ]
while line
self.log.debug "Looking at line: %p" % [ line ]
result = case line
when ECHOED_COMMAND_LINE, ''
self.log.debug "Skipping empty or echoed command line: %p" % [ line ]
self.read_queue.shift
when /^BATTERY\b/i
self.log.info "Parsing battery event."
self.parse_battery_event
when /^VERSION\b/i
self.log.info "Parsing versions event."
self.parse_versions_event
when /^GPS\b/i
self.log.info "Parsing GPS event."
self.parse_gps_event
when /^RT[12] STATE\b/i
self.log.info "Parsing channel event."
self.parse_channel_event
when /^RT[12] VOLUME\b/i
self.log.info "Parsing volume event."
self.parse_volume_event
else
self.log.debug "Unhandled line: %p" % [ line ]
self.read_queue.shift
end
return unless result
line = self.read_queue.first
end
end
Parse the given BATTERY output lines
and emit an event describing the battery status.
def parse_battery_event
data = self.parse_event_from_queue( radio: self.class.object_id ) do |line, data|
case line
when /^BATTERY HUB BOD (.*)$/
timestamp = Time.strptime( $1.strip, '%a %b %e %H:%M:%S %Y' )
data[:hub][:bod] = timestamp
when /^BATTERY HUB CAPACITY DAYS REMAINING (\d+)/
data[:hub][:capacity] = $1.to_i
when /^BATTERY TYPE (\w+)/
data[:type] = $1
when /^BATTERY MODEL (\w+)/
data[:model] = $1
when /^BATTERY CAPACITY (\w+)/
data[:capacity] = $1
when /^BATTERY STATUS (\w+)/
data[:status] = $1
when /^BATTERY CHARGE (\d+)/
data[:charge] = $1.to_i
when /^BATTERY VOLTAGE (\d+)/
data[:voltage] = $1.to_i
when /^BATTERY CURRENT (\d+)/
data[:current] = $1.to_i
when /^BATTERY TEMP (\d+)/
data[:temp] = $1.to_i
else
self.log.debug "Unhandled battery info line: %p" % [ line ]
end
end
return if data.nil? || data.empty?
self.log.info "Emitting a sys.radio.battery event: %p" % [ data ]
message = Ravn::HAL::Message.new( 'sys.radio.battery', data: )
self.filter_up( message )
end
Parse the given RT STATE output lines
and emit an event describing the transceiver’s channel state.
def parse_channel_event
data = self.parse_event_from_queue( radio: self.class.object_id ) do |line, hash|
transceiver = line[ /^RT([12])/i, 1 ] or
raise "failed to match transceiver number from %p" % [ line ]
hash[ :transceiver ] = transceiver.to_i
case line
when /^RT[12] STATE OPMODE (\S+)$/
hash[:opmode] = $1.downcase.to_sym
when /^RT[12] STATE SYS_PRESET_NAME (.+)$/
hash[:name] = $1.strip
when /^RT[12] STATE SYS_PRESET_NUMBER\s+(\d+)/
hash[:number] = $1.to_i
when /^RT[12] STATE CURRENT_WAVEFORM (.+)$/
hash[:waveform] = $1.strip
when /^RT[12] STATE OPERATIONAL_STATE (\w+)/
hash[:state] = $1.downcase.to_sym
when /^RT[12] STATE MISSION_PLAN (\S+)/
hash[:plan] = $1
when /^RT[12] STATE TYPE1-I (\S+)/
hash[:encrypted] = ($1 == 'TRUE')
when /^RT[12] STATE CIPHER_SWITCH (\w+)/
hash[:cipher_switch] = $1
when /^RT[12] STATE MODE_SWITCH (\w+)/
hash[:mode_switch] = $1
when /^RT[12] STATE EXTERNAL_PA (\w+)/
hash[:external_pa] = $1
when /^RT[12] STATE KEYLINE (\w+)/
hash[:keyline] = $1
when /^RT[12] STATE PASSWORD_STATUS (\w+)/
hash[:password_status] = $1
else
self.log.debug "Unhandled transceiver state line: %p" % [ line ]
end
end
return if data.nil? || data.empty?
self.log.info "Emitting a sys.radio.channel event: %p" % [ data ]
message = Ravn::HAL::Message.new( 'sys.radio.channel', data: )
self.filter_up( message )
end
Read GPS info from the given GPS output lines
emit an event describing the current GPS position.
def parse_gps_event
info = self.parse_event_from_queue do |line, hash|
case line
when /^GPS INFO\s*$/
when /^GPS INFO POS1 (.*)/
hash[ :pos1 ] = $1
when /^GPS INFO POS2 (.*)/
hash[ :pos2 ] = $1
when /^GPS INFO STATE (\w+)/
hash[ :state ] = $1.downcase.to_sym
when /^GPS INFO LATITUDE\s+(.+)/
hash[ :latitude ] = parse_dms_position( $1 )
when /^GPS INFO LONGITUDE\s+(.+)/
hash[ :longitude ] = parse_dms_position( $1 )
when /^GPS INFO ALTITUDE\s+(.+)/
hash[ :altitude ] = $1.to_f
when /^GPS INFO SEPARATION\s+(.+)/
hash[ :separation ] = $1.to_f
when /^GPS INFO HEADING\s+(.+)/
hash[ :heading ] = $1.to_f
when /^GPS INFO VELOCITY\s+(.+)/
hash[ :velocity ] = $1.to_f
when /^GPS INFO FOM\s+(.+)/
hash[ :fom ] = $1.to_i
when /^GPS INFO TFOM\s+(.+)/
hash[ :tfom ] = $1.to_i
when /^GPS INFO EPE\s+\+\/-\s+(.+?) m/
hash[ :epe ] = $1.to_f
when /^GPS INFO DATUM\s+(.+)/
hash[ :datum ] = $1.downcase.to_sym
when /^GPS INFO KEYSTATUS\s+(.+)/
hash[ :keystatus ] = $1.downcase.to_sym
when /^GPS INFO VALIDITY\s+(.+)/
hash[ :validity ] = $1.downcase.to_sym
when /^GPS INFO TIMESTAMP\s+\d{2}-(.+)/
self.log.warn "Ignoring bootup timestamp"
when /^GPS INFO TIMESTAMP\s+(.+)/
begin
munged_time = $1 + ' UTC'
time = Time.strptime( munged_time, '%Y-%m-%d %H:%M:%S %Z' )
hash[ :timestamp ] = time
rescue ArgumentError => err
self.log.warn "%p while parsing GPS timestamp: %s" % [ err.class, err.message ]
end
else
self.log.debug "Unknown GPS INFO line: %p" % [ line ]
end
end
return if info.nil? || info.empty?
self.log.debug "Parsed event from GPS INFO lines: %p" % [ info ]
if (message = self.build_gps_message( info ))
self.log.info "Emitting a sys.gps.position event."
self.filter_up( message )
else
self.log.warn "couldn't create GPS position message (GPS state = %p)" % [ info[:state] ]
end
if (message = self.build_gps_time_message( info ))
self.log.info "Emitting a sys.gps.time event."
self.filter_up( message )
else
self.log.warn "couldn't create GPS time message (GPS state = %p)" % [ info[:state] ]
end
end
Parse VERSION output lines
and emit an event describing various versions of hardware and software running on the radio. Returns the number of lines that were read.
def parse_versions_event
data = self.parse_event_from_queue( radio: self.class.object_id ) do |line, data|
case line
when /^VERSION ALL/
when %r{^VERSION OPTION NAME (?<name>\w+)\s+P/N (?<partno>\S+)\s+(?<desc>.+)$}
hashify_captures_into( $~, data[:options] )
when /^VERSION HW (?<name>\w+)\s+(?<sku>\w+)\s+PN (?<partno>\S+)\s+PL_REV (?<plrev>\w+)\s+PWB_REV (?<pwbrev>\w+)/
hashify_captures_into( $~, data[:hw] )
when %r{^VERSION INFOSEC (?<name>\p{Print}+)\s+P/N (?<partno>\S+)\s+REVISION (?<revision>\S+)}
hashify_captures_into( $~, data[:infosec] )
when %r{^VERSION SW (?<name>\S+)\s+P/N (?<partno>\S+)\s+REVISION (?<revision>.+?)\s+SW_REV (\S+)}
hashify_captures_into( $~, data[:sw] )
when /^VERSION SYSTEM SW\s+REVISION (\S+ \S+)/
data[:system] = $1
else
self.log.debug "Unhandled software version line: %p" % [ line ]
end
end
return if data.nil? || data.empty?
self.log.info "Emitting a sys.radio.versions event: %p" % [ data ]
message = Ravn::HAL::Message.new( 'sys.radio.versions', data: )
self.filter_up( message )
end
Parse the given RT VOLUME output lines
and emit an event describing the transceiver’s volume level.
def parse_volume_event
radio = self.class.object_id
line = self.read_queue.shift or
raise "can't read volume event: empty read queue"
line = self.read_queue.shift or return if line =~ /^RT[12] VOLUME\s*$/
self.log.warn "Parsing volume event from line: \n %p" % [ line ]
data = { radio: }
case line
when /^RT([12]) VOLUME (\d+|MIN|MAX)/
data[:transceiver] = $1.to_i
level = case $2
when 'MIN' then 0
when 'MAX' then 10
else
$2.to_i
end
data[:level] = level
else
self.log.debug "Unhandled transceiver volume line: %p" % [ line ]
end
self.log.info "Emitting a sys.radio.volume event: %p" % [ data ]
message = Ravn::HAL::Message.new( 'sys.radio.volume', data: )
self.filter_up( message )
end
queue_command( *commands )
queue_commands( *commands )
Add new commands
to the write queue.
def queue_commands( *commands )
self.write_queue.push( *commands )
end
queue_periodic_commands( * )
Timer callback: queue commands for events that should be emitted periodically.
def queue_periodic_commands( * )
self.request_gps_info
self.request_rt_info if self.class.radio_control_enabled?
end
Split off complete lines from the read_buffer
and append then to the read queue.
def queue_read_lines
self.log.debug "Queueing read lines..."
if (index = self.read_buffer.rindex( EOL ))
self.log.debug " reading lines up to index %d" % [ index ]
if (line_data = self.read_buffer.slice!( 0 .. index ))
self.log.debug " read: %p" % [ line_data ]
line_data.each_line do |line|
line.strip!
line.slice!( COMMAND_PROMPT )
next if line.empty?
if OOB_MESSAGE_LINE.match?( line )
self.handle_oob_status_line( line )
else
self.read_queue.push( line )
end
end
self.log.debug " there are now %d queued lines" % [ self.read_queue.length ]
else
self.log.warn "no line data! (0..%d of %p)" % [ index, self.read_buffer ]
end
else
self.log.debug "No complete lines in: %p." % [ self.read_buffer ]
end
end
Wait on the radio to be readable and/or writable and handle reading and writing when it’s ready.
def read_and_write
radio_dev = self.radio or raise "Couldn't connect to the radio"
ios = [ radio_dev ]
readable = ios
self.buffer_pending_writes
writable = self.write_buffer.empty? ? nil : ios
if (ready = IO.select( readable, writable, nil, IO_SELECT_TIMEOUT ))
self.log.debug "IO ready"
if ready[ 0 ]
self.read_from_radio
self.queue_read_lines
self.parse_and_emit_events
end
if ready[ 1 ]
self.write_to_radio
end
end
rescue => err
self.log.debug "%p while reading/writing to the radio: %s" %
[ err.class, err.full_message(order: :top) ]
end
Read output from the radio and generate events from it. It propagates errors from the read to its caller, notably IO::WaitReadable exceptions and EOFError.
def read_from_radio
if (data = self.radio&.read_nonblock( MAX_READ_SIZE ))
self.log.debug "Read %d bytes from the radio" % [ data.bytesize ]
self.read_buffer << data
end
rescue IO::WaitReadable, EOFError
end
Queue up the command to emit battery info events.
def request_battery_info
self.queue_command( 'BATTERY' )
end
Queue up the command to emit GPS info events.
def request_gps_info
self.queue_command( 'GPS INFO' )
end
request_next_preset( transceiver )
Queue up the command to change the given transceiver
‘s preset to the next one.
def request_next_preset( transceiver )
command = "RT%d SYS_PRESET SELECT NEXT" % [ transceiver ]
self.queue_command( command )
end
request_previous_preset( transceiver )
Queue up the command to change the given transceiver
‘s preset to the previous one.
def request_previous_preset( transceiver )
command = "RT%d SYS_PRESET SELECT PREVIOUS" % [ transceiver ]
self.queue_command( command )
end
request_rt_change( message )
def request_rt_change( message )
target_id = message.data[ :radio ]
unless target_id == self.identifier
self.log.debug "Ignoring `command` event for radio %p" % [ target_id ]
return
end
transceiver = message.data[ :transceiver ]
unless transceiver&.between?( 1, 2 )
self.log.error "Invalid/unset transceiver (%p) for message %p" % [ transceiver, message ]
return
end
case message.data[ :command ]
when 'channel up'
self.request_next_preset( transceiver )
when 'channel down'
self.request_previous_preset( transceiver )
when 'volume set'
self.request_volume_set( transceiver, message )
when nil
self.log.warn "No `command` specified for %p" % [ message ]
else
self.log.warn "Command %p not implemented." % [ message.data[:command] ]
end
end
Queue commands that will emit events describing the status of the radio transmitters (preset, volume, etc.).
def request_rt_info
self.queue_command( "RT1 STATE ALL" )
self.queue_command( "RT1 VOLUME" )
self.queue_command( "RT2 STATE ALL" )
self.queue_command( "RT2 VOLUME" )
end
Queue up the command to emit version info events.
def request_version_info
self.queue_command( 'VERSION ALL' )
end
request_volume_set( transceiver, message )
Queue up the command to change the given transceiver
‘s volume to the level specified by the message
.
def request_volume_set( transceiver, message )
if (level = message.data[ :level ])
unless level.between?( 0, 12 )
self.log.warn "Ignoring request to set volume to %p (0 <= level <=12)" %
[ level ]
return
end
else
self.log.error "No `level` given for `volume set` command: %p" % [ message ]
return
end
command = "RT%d VOLUME %d" % [ transceiver, level ]
self.queue_command( command )
end
Reset the device.
def reset( reason )
self.log.error "Reset: %p" % [ reason ]
sleep( RESET_THROTTLE_TIME )
self.core&.parent&.tell( reset_device: self )
end
Stop sending commands by canceling the timer that triggers them.
def reset_command_timer
@command_timer&.shutdown
@command_timer = self.make_command_timer
end
Start talking to the radio.
def start
self.log.info "Starting."
super
self.command_timer.execute
self.request_version_info
@io_thread = self.start_io_loop unless self.class.simulation_mode?
end
Start reading and writing from/to the radio. Emits events for information read, and writes commands from the write_queue
if there are any. Returns a Thread that is running the loop.
def start_io_loop
self.log.info "Starting the IO loop thread."
return Thread.new( &self.method(:io_loop) )
end
Stop talking to the radio.
def stop
self.log.info "Stopping."
self.reset_command_timer
self.log.warn "Closing the radio device and shutting down the IO thread."
@radio&.close unless @radio&.closed?
@radio = nil
@io_thread&.join( IO_THREAD_WAIT_TIME )
@io_thread = nil
end
Write any pending commands to the radio.
def write_to_radio
return if self.write_buffer.empty?
self.log.debug "Trying to write %d bytes to the radio" % [ self.write_buffer.bytesize ]
bytes = self.radio&.write_nonblock( self.write_buffer )
if bytes&.nonzero?
self.log.debug "wrote %d bytes to the radio" % [ bytes ]
self.write_buffer.slice!( 0, bytes )
end
rescue IO::WaitWritable
end
Protected Instance Methods
Build a Concurrent::TimerTask that will read output from the radio and generate events from it.
def make_command_timer
timer = Concurrent::TimerTask.new { self.queue_periodic_commands }
timer.execution_interval = self.class.status_frequency
timer.add_observer( Ravn::LoggingTaskObserver.new(:prc163_command_timer) )
return timer
end
parse_event_from_queue( **initial_data ) { |line, data| ... }
Iterate line-by-line through the current read buffer, yielding each line and a Hash of accumulated data to the block, prepopulated with the given initial_data
. Returns the accumulated data Hash back to the caller if successful, or nil
if not successful at parsing.
def parse_event_from_queue( **initial_data )
raise LocalJumpError, "no block given" unless block_given?
unless self.read_queue_contains_list_ending?
self.log.debug "Partial output in read buffer, skipping."
sleep 2.0
return nil
end
data = Hash.new( &AUTOVIVIFY )
data.merge!( initial_data )
line = self.read_queue.shift
while line
if line.match?( LIST_ENDING_MARKER )
self.log.debug "Found the list ending marker."
break
end
yield( line, data )
line = self.read_queue.shift
end
return data
end
read_queue_contains_list_ending?()
Returns true
if the current read queue contains one or more list-ending markers.
def read_queue_contains_list_ending?
return self.read_queue.any? {|line| line.match?(LIST_ENDING_MARKER) }
end