Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,19 @@ COMMAND <%= target_name %> BLOCK BIG_ENDIAN "Send variable block of data"
HIDDEN
RELATED_ITEM <%= target_name %> HEALTH_STATUS BLOCKTEST

COMMAND <%= target_name %> MEMDUMP BIG_ENDIAN "Dump a block of simulated memory"
<%= render "_ccsds_cmd.txt", locals: {id: 17} %>
APPEND_PARAMETER OFFSET 32 UINT MIN MAX 0 "Memory offset in bytes to start the dump"
APPEND_PARAMETER SIZE 32 UINT 1 4096 1024 "Number of bytes to dump"
UNITS Bytes B
APPEND_PARAMETER BAD_CRC 8 UINT 0 1 0 "Flip a random bit in the response DATA after the CRC is calculated to simulate a bad CRC"
STATE FALSE 0
STATE TRUE 1
APPEND_PARAMETER CRC 32 UINT MIN MAX 0 "CRC32 over OFFSET, SIZE and BAD_CRC (calculated automatically, use cmd_raw to override)"
FORMAT_STRING "0x%08X"
WRITE_CONVERSION memdump_cmd_crc_conversion.rb
RELATED_ITEM <%= target_name %> MEMDUMP DATA

# Throughput Testing Commands (for use with external throughput server)
COMMAND <%= target_name %> START_STREAM BIG_ENDIAN "Start telemetry streaming from throughput server"
<%= render "_ccsds_cmd.txt", locals: {id: 200} %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,30 @@ TELEMETRY <%= target_name %> VARIABLE_ARRAYS BIG_ENDIAN "Variable length arrays"
READ_CONVERSION openc3/conversions/unix_time_conversion.py TIMESEC TIMEUS
CONVERTED_DATA 280 STRING

TELEMETRY <%= target_name %> MEMDUMP BIG_ENDIAN "Memory dump sent in response to the MEMDUMP command"
<%= render "_ccsds_tlm.txt", locals: {apid: 8} %>
APPEND_ITEM OFFSET 32 UINT "Memory offset in bytes of the dump"
APPEND_ITEM SIZE 32 UINT "Number of bytes in the dump"
UNITS Bytes B
APPEND_ITEM DATA 0 BLOCK "Dumped memory"
VARIABLE_BIT_SIZE SIZE
APPEND_ITEM CRC 32 UINT "CRC32 of DATA calculated by the target"
FORMAT_STRING "0x%08X"
ITEM CALC_CRC 0 0 DERIVED "CRC32 of DATA calculated on the ground"
READ_CONVERSION crc32_conversion.rb DATA
CONVERTED_DATA 32 UINT
FORMAT_STRING "0x%08X"
ITEM CRC_OK 0 0 DERIVED "Whether the target CRC matches the ground CRC"
GENERIC_READ_CONVERSION_START
packet.read('CRC') == packet.read('CALC_CRC') ? 1 : 0
GENERIC_READ_CONVERSION_END
CONVERTED_DATA 8 UINT
STATE TRUE 1 GREEN
STATE FALSE 0 RED
ITEM PACKET_TIME 0 0 DERIVED "Ruby time based on TIMESEC and TIMEUS"
READ_CONVERSION unix_time_conversion.rb TIMESEC TIMEUS
CONVERTED_DATA 280 STRING

TELEMETRY <%= target_name %> JSONTLM BIG_ENDIAN "JSON Accessor Telemetry"
ACCESSOR JsonAccessor
# Template is not required for telemetry, but is useful for simulation
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# encoding: ascii-8bit

# Copyright 2026 OpenC3, Inc.
# All Rights Reserved.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See LICENSE.md for more details.
#
# This file may also be used under the terms of a commercial license
# if purchased from OpenC3, Inc.

require 'openc3/conversions/conversion'
require 'openc3/utilities/crc'

module OpenC3
# Read conversion which calculates the CRC32 of another item in the packet.
# Used to verify the CRC sent by the target.
# See https://docs.openc3.com/docs/configuration/conversions
class Crc32Conversion < Conversion
# @param item_name [String] Name of the item to calculate the CRC over
def initialize(item_name)
super()
@item_name = item_name.to_s.upcase
@crc = Crc32.new
@params = [@item_name]
end

# @param value [Object] Value based on the item definition. This could be
# a string, integer, float, or array of values.
# @param packet [Packet] The packet object where the conversion is defined
# @param buffer [String] The raw packet buffer
def call(_value, packet, buffer)
@crc.calc(packet.read(@item_name, :RAW, buffer))
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# encoding: ascii-8bit

# Copyright 2026 OpenC3, Inc.
# All Rights Reserved.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See LICENSE.md for more details.
#
# This file may also be used under the terms of a commercial license
# if purchased from OpenC3, Inc.

require 'openc3/conversions/conversion'
require 'openc3/utilities/crc'

module OpenC3
# Write conversion which calculates the MEMDUMP command CRC32 over the
# big endian OFFSET, SIZE and BAD_CRC parameters so the user never has to.
# Send the command with cmd_raw to bypass this conversion and set a bad CRC.
# See https://docs.openc3.com/docs/configuration/conversions
class MemdumpCmdCrcConversion < Conversion
def initialize
super()
@crc = Crc32.new
end

# @param value [Object] Value based on the item definition. This could be
# a string, integer, float, or array of values.
# @param packet [Packet] The packet object where the conversion is defined
# @param buffer [String] The raw packet buffer
def call(_value, packet, _buffer)
# Write conversions run before the given parameters are written to the
# buffer so use the given values and fall back to the defaults.
# Given names keep the caller's case so upcase them to match the items.
given = (packet.given_values || {}).transform_keys { |key| key.to_s.upcase }
offset = Integer(given['OFFSET'] || packet.get_item('OFFSET').default)
size = Integer(given['SIZE'] || packet.get_item('SIZE').default)
# BAD_CRC may be given as a state name so convert it to its value
bad_crc_item = packet.get_item('BAD_CRC')
bad_crc = given['BAD_CRC'] || bad_crc_item.default
bad_crc = bad_crc_item.states[bad_crc.to_s.upcase] || bad_crc
@crc.calc([offset, size, Integer(bad_crc)].pack('NNC'))
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
require 'stringio'
require 'base64'
require 'openc3/accessors/binary_accessor'
require 'openc3/utilities/crc'

module OpenC3
# Simulated instrument for the demo. Populates several packets and cycles
Expand Down Expand Up @@ -128,6 +129,10 @@ def initialize(target_name)
packet.CcsdsSeqFlags = 'NOGROUP'
packet.CcsdsLength = packet.buffer.length - 7

packet = @tlm_packets['MEMDUMP']
packet.enable_method_missing
packet.CcsdsSeqFlags = 'NOGROUP'

@solar_panel_positions = SOLAR_PANEL_DFLTS.dup
@solar_panel_thread = nil
@solar_panel_thread_cancel = false
Expand All @@ -150,6 +155,8 @@ def initialize(target_name)
@time_offset = 0
@ip_address = 0
@variable_arrays_updated = false
@memdump_response = nil
@crc = Crc32.new
end

def set_rates
Expand Down Expand Up @@ -269,6 +276,26 @@ def write(packet)
# Write only arrays - variable_bit_size will auto-update length fields
arrays_packet.write('ARRAY1', packet.read('ARRAY1'))
arrays_packet.write('ARRAY2', packet.read('ARRAY2'))
when 'MEMDUMP'
offset = packet.read('OFFSET')
size = packet.read('SIZE')
bad_crc = packet.read('BAD_CRC', :RAW)
# Reject the command if the CRC over OFFSET, SIZE and BAD_CRC doesn't match
return unless packet.read('CRC') == @crc.calc([offset, size, bad_crc].pack('NNC'))
# Reject sizes outside the command's range since cmd_raw skips range checks
return unless packet.get_item('SIZE').range.cover?(size)
hs_packet.cmd_acpt_cnt += 1
# Simulated memory is a byte counter where each byte holds its address
data = Array.new(size) { |i| (offset + i) & 0xFF }.pack('C*')
crc = @crc.calc(data)
if bad_crc == 1
# Flip a random bit after calculating the CRC to simulate downlink corruption
bit = rand(size * 8)
data.setbyte(bit / 8, data.getbyte(bit / 8) ^ (0x80 >> (bit % 8)))
end
# Commands arrive on a different thread than read so only hand off the
# values here and let read write them into the MEMDUMP packet
@memdump_response = [offset, data, crc]
end
end

Expand Down Expand Up @@ -469,6 +496,21 @@ def read(count_100hz, time)
pending_packets << arrays_packet
end

memdump_response, @memdump_response = @memdump_response, nil
if memdump_response
offset, data, crc = memdump_response
memdump_packet = @tlm_packets['MEMDUMP']
memdump_packet.write('OFFSET', offset)
# Writing DATA resizes the packet and updates SIZE due to VARIABLE_BIT_SIZE
memdump_packet.write('DATA', data)
memdump_packet.write('CRC', crc)
memdump_packet.write('CCSDSLENGTH', memdump_packet.buffer(false).length - 7)
memdump_packet.write('TIMESEC', time.tv_sec - @time_offset)
memdump_packet.write('TIMEUS', time.tv_usec)
memdump_packet.write('CCSDSSEQCNT', memdump_packet.read('CCSDSSEQCNT') + 1)
pending_packets << memdump_packet
end

pending_packets
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Copyright 2026 OpenC3, Inc.
# All Rights Reserved.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See LICENSE.md for more details.
#
# This file may also be used under the terms of a commercial license
# if purchased from OpenC3, Inc.

# Dump a region of INST memory with the MEMDUMP command, verify each chunk
# and then push the dump to the bucket with put_target_file.
#
# The dump recovers from a bad downlink:
# * Chunks with no response are requested again
# * Chunks with a bad CRC prompt the operator to retry or stop the dump
# * If the link is down the script waits for it to come back
#
# Environment variables:
# BAD_CRC=1 flips a bit in one chunk part way through the dump to show recovery from a bad CRC
require 'openc3/utilities/crc'

START_OFFSET = 0
TOTAL_SIZE = 10000 # Bytes to dump
CHUNK_SIZE = 1024 # Bytes per MEMDUMP command (max 4096)
MAX_ATTEMPTS = 3 # Times to request a chunk with no response before giving up
RESPONSE_TIMEOUT = 5 # Seconds to wait for each MEMDUMP response
LINK_TIMEOUT = 60 # Seconds to wait for a lost link to come back

crc = OpenC3::Crc32.new
bad_crc = ENV.fetch('BAD_CRC', nil)
end_offset = START_OFFSET + TOTAL_SIZE
dump_data = ''.b # The verified dump is small enough to keep in memory
offset = START_OFFSET
bucket_path = "INST/memdumps/INST_memdump_#{Time.now.utc.strftime('%Y%m%d%H%M%S')}.bin"

bad_crc_offset = START_OFFSET + (TOTAL_SIZE / CHUNK_SIZE / 2) * CHUNK_SIZE
while offset < end_offset
size = [CHUNK_SIZE, end_offset - offset].min
# Request a bad CRC on the first request for the chunk in the middle of
# the dump
chunk_bad_crc = 'FALSE'
if bad_crc and offset >= bad_crc_offset
chunk_bad_crc = 'TRUE'
bad_crc = nil
end

data = nil
attempt = 0
while data.nil? and attempt < MAX_ATTEMPTS
attempt += 1
# Record the received count so we know when the response arrives
count = tlm("INST MEMDUMP RECEIVED_COUNT") || 0

# Send the command, waiting for the link if it is down. This doesn't use up
# an attempt. An ack timeout may mean the command did reach the target but
# the duplicate response is harmless because it has the same OFFSET and SIZE.
link_deadline = Time.now + LINK_TIMEOUT
sent = false
until sent
begin
# The command CRC over OFFSET, SIZE and BAD_CRC is calculated automatically
cmd("INST MEMDUMP with OFFSET #{offset}, SIZE #{size}, BAD_CRC #{chunk_bad_crc}")
sent = true
# Only the first request has a bad CRC so the retry succeeds
chunk_bad_crc = 'FALSE'
rescue => e
raise unless e.message =~ /not connected|waiting for cmd ack/i and Time.now < link_deadline
puts "WARN: #{e.message}, retrying until the link returns"
wait(2)
end
end

# Wait for the response to this request. A stale response (e.g. a late
# reply to an earlier request) is skipped and we keep waiting.
dump = nil
deadline = Time.now + RESPONSE_TIMEOUT
while (remaining = deadline - Time.now) > 0
# wait returns false on timeout rather than raising
break unless wait("INST MEMDUMP RECEIVED_COUNT > #{count}", remaining)
# Grab all the items at once so they all come from the same packet
response = get_tlm_packet("INST MEMDUMP", type: :RAW).to_h { |name, value, _| [name, value] }
count = response['RECEIVED_COUNT']
if response['OFFSET'] == offset and response['SIZE'] == size
dump = response
break
end
puts "WARN: Ignoring stale response for OFFSET #{response['OFFSET']}, SIZE #{response['SIZE']} " \
"while waiting for OFFSET #{offset}, SIZE #{size}"
end
unless dump
puts "WARN: No response for OFFSET #{offset} (attempt #{attempt} of #{MAX_ATTEMPTS})"
next
end

# Calculate the CRC in the script (the CRC_OK derived item does the same
# check on the ground for display on the screen)
calculated = crc.calc(dump['DATA'])
if calculated == dump['CRC']
puts "CRC passed at OFFSET #{offset}"
data = dump['DATA']
else
mismatch = "Target CRC 0x#{dump['CRC'].to_s(16).upcase} vs calculated 0x#{calculated.to_s(16).upcase}"
puts "WARN: CRC mismatch at OFFSET #{offset}: #{mismatch}"
# Let the operator decide whether to request the chunk again
answer = message_box("Bad CRC at OFFSET #{offset}. Retry this chunk?", 'Retry', 'Stop Dump', informative: mismatch)
if answer == 'Stop Dump'
raise "Dump stopped by operator at OFFSET #{offset}"
end
# An operator retry doesn't use up an attempt
attempt -= 1
end
end
raise "Unable to dump OFFSET #{offset} after #{MAX_ATTEMPTS} attempts" unless data
dump_data << data.b
offset += size
end
puts "Dumped #{dump_data.length} bytes (CRC32 0x#{crc.calc(dump_data).to_s(16).upcase})"

# Push the dump to the bucket
put_target_file(bucket_path, dump_data)

# Read it back to verify it made it to the bucket intact
file = get_target_file(bucket_path)
data = file.read
file.delete
check_expression("#{data.length} == #{TOTAL_SIZE}")
check_expression("#{crc.calc(data)} == #{crc.calc(Array.new(TOTAL_SIZE) { |i| (START_OFFSET + i) & 0xFF }.pack('C*'))}")
puts "Memory dump saved to #{bucket_path}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
SCREEN AUTO AUTO 1.0

TITLE "<%= target_name %> Memory Dump"

HORIZONTAL
VERTICALBOX "Commands"
MATRIXBYCOLUMNS 2
LABEL "Offset:"
NAMED_WIDGET OFFSET TEXTFIELD 10 "0"
LABEL "Size:"
NAMED_WIDGET SIZE TEXTFIELD 10 "256"
END
# Flips a random bit in the dump after the target calculates its CRC
NAMED_WIDGET BAD_CRC CHECKBUTTON 'Bad CRC'
# The command CRC over OFFSET, SIZE and BAD_CRC is calculated automatically
BUTTON 'Dump Memory' "var offset=screen.getNamedWidget('OFFSET').text();" +
"var size=screen.getNamedWidget('SIZE').text();" +
"var badCrc=screen.getNamedWidget('BAD_CRC').checked() ? 'TRUE' : 'FALSE';" +
"api.cmd('<%= target_name %> MEMDUMP with OFFSET '+offset+', SIZE '+size+', BAD_CRC '+badCrc)"
# cmd_raw skips the CRC write conversion so the target rejects the command
BUTTON 'Run Dump Script' "runScript('<%= target_name %>/procedures/memdump.rb', true)"
BUTTON 'Run Dump Script with Bad CRC' "runScript('<%= target_name %>/procedures/memdump.rb', true, {'BAD_CRC': '1'})"
END

VERTICALBOX "Last Dump"
LABELVALUE <%= target_name %> MEMDUMP OFFSET
LABELVALUE <%= target_name %> MEMDUMP SIZE
LABELVALUE <%= target_name %> MEMDUMP CRC
LABELVALUE <%= target_name %> MEMDUMP CALC_CRC
LABELLED <%= target_name %> MEMDUMP CRC_OK
SETTING LED_COLOR TRUE GREEN
SETTING LED_COLOR FALSE RED
LABELVALUE <%= target_name %> MEMDUMP RECEIVED_COUNT
LABELVALUE <%= target_name %> MEMDUMP PACKET_TIME
END
END

VERTICALBOX "Data"
BLOCK <%= target_name %> MEMDUMP DATA 680 200 "%02X" 4 8 "0x%08X: "
END
Loading
Loading