openc3 6.10.2 → 6.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5cc7320f928c8941c9d4ecc2a4bc04eade3ebde86c278dfdad90aa7ac24f9d2
4
- data.tar.gz: eb9cd8b34986f982c187dbccbdf0c4067f61df733161cab067a5c689b1ba53a9
3
+ metadata.gz: c1459e2b7d8160f09840d33a7fc6aa6314eb34367d68f41d45902a0203b37f57
4
+ data.tar.gz: 6de6d85032257c4e19f9470846056d5cf4aa8acc0498e4f9691543598a12856f
5
5
  SHA512:
6
- metadata.gz: 5973122351e2b191d365092984db9c23ad877c8fc71c7173b014fa3a5ae363258a2eeca7863571d9206be0f781ff361586843eac57d7fa56065fef515398c06b
7
- data.tar.gz: 7b664d2a9a8615a5c651002b412cd7248ca0b33e67f6af0618b04354f3ea080b5d81f7d8031ea4e98a013ca2ca4714e3fdb6b30ae5302223d7bdafd88d8c63bb
6
+ metadata.gz: 556297be7a37dac84357e9a4c63c0c69a8c2fae6b0f8478317c9eb5b95506880f2b4acb2c99a56e4d7d2b0e4c4f22f4cf78a244046f027b68aad6b73a87a7845
7
+ data.tar.gz: e36738a22fcdcfbc136a899a051f691c29b8a0701b6c6a6f79d36c3f4689d4ea8018c01b90a5908b082a07507d14f2fce3ab252fcd5abc3a04312daf935f1cfc
data/bin/openc3cli CHANGED
@@ -33,6 +33,7 @@ require 'openc3/models/queue_model'
33
33
  require 'openc3/models/scope_model'
34
34
  require 'openc3/models/tool_model'
35
35
  require 'openc3/packets/packet_config'
36
+ require 'openc3/packets/parsers/xtce_converter'
36
37
  require 'openc3/utilities/bucket'
37
38
  require 'openc3/utilities/cli_generator'
38
39
  require 'openc3/utilities/local_mode'
@@ -107,7 +108,8 @@ def xtce_converter(args)
107
108
  options = {}
108
109
  option_parser = OptionParser.new do |opts|
109
110
  opts.banner = "Usage: xtce_converter [options] --import input_xtce_filename --output output_dir\n"+
110
- " xtce_converter [options] --plugin /PATH/FILENAME.gem --output output_dir --variables variables.txt"
111
+ " xtce_converter [options] --plugin /PATH/FILENAME.gem --output output_dir "+
112
+ " --variables variables.txt --root_target root_target_name --time_association_name time_association_name"
111
113
  opts.separator("")
112
114
  opts.on("-h", "--help", "Show this message") do
113
115
  puts opts
@@ -125,6 +127,12 @@ def xtce_converter(args)
125
127
  opts.on("-v", "--variables", "Optional variables file to pass to the plugin") do |arg|
126
128
  options[:variables] = arg
127
129
  end
130
+ opts.on("-r ROOT_TARGET", "--root_target ROOT_TARGET", "Optional flag to set which target is at the root of an XTCE document. If not specified, each target will be placed under a generic 'root' spacesystem") do |arg|
131
+ options[:root_target_name] = arg
132
+ end
133
+ opts.on("-t TIME_ASSOCIATION_NAME", "--time_association_name TIME_ASSOCIATION_NAME", "Optional flag to set which target is at the root of an XTCE document. If not specified, each target will be placed under a generic 'root' spacesystem") do |arg|
134
+ options[:time_association_name] = "PACKET_TIME"
135
+ end
128
136
  end
129
137
 
130
138
  begin
@@ -156,8 +164,10 @@ def xtce_converter(args)
156
164
  puts "Installing #{File.basename(options[:plugin])}"
157
165
  plugin_hash = OpenC3::PluginModel.install_phase1(options[:plugin], existing_variables: variables, scope: 'DEFAULT', validate_only: true)
158
166
  plugin_hash['variables']['xtce_output'] = options[:output]
167
+ plugin_hash['variables']['time_association_name'] = options[:time_association_name]
159
168
  OpenC3::PluginModel.install_phase2(plugin_hash, scope: 'DEFAULT', validate_only: true,
160
169
  gem_file_path: options[:plugin])
170
+ OpenC3::XtceConverter.combine_output_xtce(options[:output], options[:root_target_name])
161
171
  result = 0 # bash and Windows consider 0 success
162
172
  rescue => e
163
173
  puts "Error: #{e.message}"
@@ -79,12 +79,12 @@ GENERIC_READ_CONVERSION_START:
79
79
  ruby_example: |
80
80
  APPEND_ITEM ITEM1 32 UINT
81
81
  GENERIC_READ_CONVERSION_START
82
- return (value * 1.5).to_i # Convert the value by a scale factor
82
+ (value * 1.5).to_i # Convert the value by a scale factor
83
83
  GENERIC_READ_CONVERSION_END
84
84
  python_example: |
85
85
  APPEND_ITEM ITEM1 32 UINT
86
86
  GENERIC_READ_CONVERSION_START
87
- return int(value * 1.5) # Convert the value by a scale factor
87
+ int(value * 1.5) # Convert the value by a scale factor
88
88
  GENERIC_READ_CONVERSION_END
89
89
  parameters:
90
90
  - name: Converted Type
@@ -141,12 +141,12 @@ GENERIC_WRITE_CONVERSION_START:
141
141
  ruby_example: |
142
142
  APPEND_PARAMETER ITEM1 32 UINT 0 0xFFFFFFFF 0
143
143
  GENERIC_WRITE_CONVERSION_START
144
- return (value * 1.5).to_i # Convert the value by a scale factor
144
+ (value * 1.5).to_i # Convert the value by a scale factor
145
145
  GENERIC_WRITE_CONVERSION_END
146
146
  python_example: |
147
147
  APPEND_PARAMETER ITEM1 32 UINT 0 0xFFFFFFFF 0
148
148
  GENERIC_WRITE_CONVERSION_START
149
- return int(value * 1.5) # Convert the value by a scale factor
149
+ int(value * 1.5) # Convert the value by a scale factor
150
150
  GENERIC_WRITE_CONVERSION_END
151
151
  GENERIC_WRITE_CONVERSION_END:
152
152
  summary: Complete a generic write conversion
@@ -125,6 +125,29 @@ SUBSETTING:
125
125
  LABELVALUELIMITSBAR INST HEALTH_STATUS TEMP1
126
126
  SUBSETTING 0 TEXTCOLOR green # Change the label's text to green
127
127
  END
128
+ TOOLTIP:
129
+ summary: Adds a tooltip to the previously defined widget
130
+ description: TOOLTIP applies a custom hover tooltip to the widget defined immediately before it.
131
+ This allows you to provide helpful descriptions, mnemonics, or other contextual information
132
+ that appears when the user hovers over a widget. The tooltip overrides any default tooltip
133
+ that the widget may have.
134
+ since: 6.10.3
135
+ parameters:
136
+ - name: Tooltip Text
137
+ required: true
138
+ description: The text to display in the tooltip when hovering over the widget.
139
+ values: .+
140
+ - name: Delay
141
+ required: false
142
+ description: The delay in milliseconds before the tooltip appears (default = 600).
143
+ values: \d+
144
+ example: |
145
+ LED INST PARAMS VALUE5 RAW 25 20
146
+ SETTING LED_COLOR 0 GREEN
147
+ SETTING LED_COLOR 1 RED
148
+ TOOLTIP "Mnemonic: ABCDEF. This is the Star Tracker On/Off Status"
149
+ VALUE INST HEALTH_STATUS TEMP1
150
+ TOOLTIP "Temperature sensor 1: Primary thermal control" 1000
128
151
  NAMED_WIDGET:
129
152
  summary: Name a widget to allow access to it via the getNamedWidget method
130
153
  description: To programmatically access parts of a telemetry screen you need
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  TARGET:
3
3
  summary: Defines a new target
4
- example: TARGET INST INST
4
+ example: TARGET KEYSIGHT_N6700 PWR_SUPPLY1
5
5
  parameters:
6
6
  - name: Folder Name
7
7
  required: true
@@ -10,8 +10,13 @@ TARGET:
10
10
  - name: Name
11
11
  required: true
12
12
  description:
13
- The target name. While this is almost always the same as Folder Name
14
- it can be different to create multiple targets based on the same target folder.
13
+ The target name. While this typically matches the Folder Name
14
+ it can be different to create multiple targets based on the same target definition.
15
+ As in the Example Usage, the target folder is KEYSIGHT_N6700 but the target name is PWR_SUPPLY1.
16
+ To create multiple targets from the same folder, just define multiple TARGET entries
17
+ with different target names. To make the target definition flexbible, you can use ERB to
18
+ insert the target name in procedures, libraries, etc via <%= target_name %>.
19
+ See [ERB target_name](/docs/configuration/format#target_name) for more information.
15
20
  values: .*
16
21
  modifiers:
17
22
  CMD_BUFFER_DEPTH:
@@ -204,6 +204,36 @@ Decoration Widgets:
204
204
  SPACER 0 100
205
205
  LABEL "Spacer above"
206
206
  END
207
+ FILEDISPLAY:
208
+ summary: Displays the contents of a target file with syntax highlighting
209
+ since: 6.10.3
210
+ parameters:
211
+ - name: File path
212
+ required: true
213
+ description: Path to the file relative to the target folder (e.g. "INST/procedures/file.rb")
214
+ values: .+
215
+ - name: Width
216
+ required: false
217
+ description: Width of the widget in pixels (default = 600)
218
+ values: \d+
219
+ - name: Height
220
+ required: false
221
+ description: Height of the widget in pixels (default = 300)
222
+ values: \d+
223
+ example: |
224
+ FILEDISPLAY "INST/data/sample.json" 400 200
225
+ FILECHECKSUM:
226
+ summary: Displays SHA-256 checksum of one or more files, with comparison if multiple
227
+ since: 6.10.3
228
+ parameters:
229
+ - name: File path
230
+ required: true
231
+ description: Path to a file relative to the target folder (e.g. "INST/procedures/file.rb"). Multiple file paths can be provided to compare checksums.
232
+ values: .+
233
+ example: |
234
+ FILECHECKSUM "INST/data/sample.json"
235
+ FILECHECKSUM "INST/data/sample.json" "INST2/data/sample.json"
236
+ FILECHECKSUM "INST/data/file1.bin" "INST/data/file2.bin" "INST/data/file3.bin"
207
237
  Telemetry Widgets:
208
238
  description: Telemetry widgets are used to display telemetry values.
209
239
  The first parameters to each of these widgets is a telemetry mnemonic.
@@ -25,7 +25,7 @@ require 'openc3/accessors/accessor'
25
25
  OpenC3.disable_warnings do
26
26
  class JsonPath
27
27
  def self.process_object(obj_or_str, opts = {})
28
- obj_or_str.is_a?(String) ? MultiJson.decode(obj_or_str, max_nesting: opts[:max_nesting], create_additions: true, allow_nan: true) : obj_or_str
28
+ obj_or_str.is_a?(String) ? MultiJson.load(obj_or_str, max_nesting: opts[:max_nesting], create_additions: true, allow_nan: true) : obj_or_str
29
29
  end
30
30
  end
31
31
  end
@@ -284,13 +284,8 @@ module OpenC3
284
284
  value_type = 'RAW' # Must request the raw value when dealing with the reserved items
285
285
  end
286
286
 
287
- # QuestDB 9.0.0 only supports DOUBLE arrays: https://questdb.com/docs/concept/array/
287
+ # Arrays must be accessed as RAW since there's no conversion
288
288
  if item['array_size']
289
- # TODO: This needs work ... we're JSON encoding non numeric array values
290
- if item['data_type'] == 'STRING' or item['data_type'] == 'BLOCK'
291
- results << nil
292
- next
293
- end
294
289
  value_type = 'RAW'
295
290
  end
296
291
 
@@ -34,6 +34,10 @@ require 'faraday'
34
34
 
35
35
 
36
36
  module OpenC3
37
+ # Number of times to retry a request when a connection error occurs
38
+ RETRY_COUNT = 3
39
+ # Delay between retries in seconds
40
+ RETRY_DELAY = 0.1
37
41
 
38
42
  class JsonApiError < StandardError; end
39
43
 
@@ -200,21 +204,41 @@ module OpenC3
200
204
 
201
205
  # NOTE: This is a helper method and should not be called directly
202
206
  def _send_request(method:, endpoint:, kwargs:)
203
- begin
204
- uri = URI("#{@url}#{endpoint}")
205
- @log[0] = "#{method} Request: #{uri.to_s} #{kwargs}"
206
- STDOUT.puts @log[0] if JsonDRb.debug?
207
- resp = _http_request(method: method, uri: uri, kwargs: kwargs)
208
- @log[1] = "#{method} Response: #{resp.status} #{resp.headers} #{resp.body}"
209
- STDOUT.puts @log[1] if JsonDRb.debug?
210
- @response_data = resp.body
211
- return resp
212
- rescue StandardError => e
213
- @log[2] = "#{method} Exception: #{e.class}, #{e.message}, #{e.backtrace}"
214
- disconnect()
215
- error = "Api Exception: #{@log[0]} ::: #{@log[1]} ::: #{@log[2]}"
216
- raise error
207
+ uri = URI("#{@url}#{endpoint}")
208
+ @log[0] = "#{method} Request: #{uri.to_s} #{kwargs}"
209
+ STDOUT.puts @log[0] if JsonDRb.debug?
210
+
211
+ retry_count = 0
212
+ while retry_count <= RETRY_COUNT
213
+ begin
214
+ resp = _http_request(method: method, uri: uri, kwargs: kwargs)
215
+ @log[1] = "#{method} Response: #{resp.status} #{resp.headers} #{resp.body}"
216
+ STDOUT.puts @log[1] if JsonDRb.debug?
217
+ @response_data = resp.body
218
+ return resp
219
+ rescue Faraday::ConnectionFailed, Errno::ECONNRESET, Errno::EPIPE, IOError => e
220
+ # Connection errors are retryable - reconnect and try again
221
+ retry_count += 1
222
+ @log[2] = "#{method} Exception: #{e.class}, #{e.message}, #{e.backtrace}"
223
+ if retry_count <= RETRY_COUNT
224
+ Logger.warn("JsonApiObject: Connection error, retry #{retry_count}/#{RETRY_COUNT}: #{e.class} #{e.message}")
225
+ disconnect()
226
+ sleep(RETRY_DELAY)
227
+ connect()
228
+ else
229
+ error = "Api Exception: #{@log[0]} ::: #{@log[1]} ::: #{@log[2]}"
230
+ raise error
231
+ end
232
+ rescue StandardError => e
233
+ @log[2] = "#{method} Exception: #{e.class}, #{e.message}, #{e.backtrace}"
234
+ disconnect()
235
+ error = "Api Exception: #{@log[0]} ::: #{@log[1]} ::: #{@log[2]}"
236
+ raise error
237
+ end
217
238
  end
239
+ # Should not reach here, but just in case
240
+ error = "Api Exception: #{@log[0]} ::: #{@log[1]} ::: #{@log[2]}"
241
+ raise error
218
242
  end
219
243
 
220
244
  # NOTE: This is a helper method and should not be called directly
@@ -92,18 +92,36 @@ module OpenC3
92
92
  'Content-Type' => 'application/json-rpc',
93
93
  }
94
94
  end
95
- begin
96
- @log[0] = "Request: #{@uri.to_s} #{USER_AGENT} #{data.to_s}"
97
- STDOUT.puts @log[0] if JsonDRb.debug?
98
- resp = @http.post(@uri, data, headers)
99
- @log[1] = "Response: #{resp.status} #{resp.headers} #{resp.body}"
100
- @response_data = resp.body
101
- STDOUT.puts @log[1] if JsonDRb.debug?
102
- return resp.body
103
- rescue StandardError => e
104
- @log[2] = "Exception: #{e.class}, #{e.message}, #{e.backtrace}"
105
- return nil
95
+
96
+ @log[0] = "Request: #{@uri.to_s} #{USER_AGENT} #{data.to_s}"
97
+ STDOUT.puts @log[0] if JsonDRb.debug?
98
+
99
+ retry_count = 0
100
+ while retry_count <= RETRY_COUNT
101
+ begin
102
+ resp = @http.post(@uri, data, headers)
103
+ @log[1] = "Response: #{resp.status} #{resp.headers} #{resp.body}"
104
+ @response_data = resp.body
105
+ STDOUT.puts @log[1] if JsonDRb.debug?
106
+ return resp.body
107
+ rescue Faraday::ConnectionFailed, Errno::ECONNRESET, Errno::EPIPE, IOError => e
108
+ # Connection errors are retryable - reconnect and try again
109
+ retry_count += 1
110
+ @log[2] = "Exception: #{e.class}, #{e.message}, #{e.backtrace}"
111
+ if retry_count <= RETRY_COUNT
112
+ Logger.warn("JsonDRbObject: Connection error, retry #{retry_count}/#{RETRY_COUNT}: #{e.class} #{e.message}")
113
+ disconnect()
114
+ sleep(RETRY_DELAY)
115
+ connect()
116
+ else
117
+ return nil
118
+ end
119
+ rescue StandardError => e
120
+ @log[2] = "Exception: #{e.class}, #{e.message}, #{e.backtrace}"
121
+ return nil
122
+ end
106
123
  end
124
+ return nil
107
125
  end
108
126
 
109
127
  def handle_response(response:)
@@ -89,6 +89,7 @@ module OpenC3
89
89
  InterfaceTopic.receive_commands(@interface, scope: @scope) do |topic, msg_id, msg_hash, _redis|
90
90
  OpenC3.with_context(msg_hash) do
91
91
  release_critical = false
92
+ critical_model = nil
92
93
  msgid_seconds_from_epoch = msg_id.split('-')[0].to_i / 1000.0
93
94
  delta = Time.now.to_f - msgid_seconds_from_epoch
94
95
  @metric.set(name: 'interface_topic_delta_seconds', value: delta, type: 'gauge', unit: 'seconds', help: 'Delta time between data written to stream and interface cmd start') if @metric
@@ -188,9 +189,9 @@ module OpenC3
188
189
  end
189
190
  if msg_hash.key?('release_critical')
190
191
  # Note: intentional fall through below this point
191
- model = CriticalCmdModel.get_model(name: msg_hash['release_critical'], scope: @scope)
192
- if model
193
- msg_hash = model.cmd_hash
192
+ critical_model = CriticalCmdModel.get_model(name: msg_hash['release_critical'], scope: @scope)
193
+ if critical_model
194
+ msg_hash = critical_model.cmd_hash
194
195
  release_critical = true
195
196
  else
196
197
  next "Critical command #{msg_hash['release_critical']} not found"
@@ -279,6 +280,10 @@ module OpenC3
279
280
  command.extra ||= {}
280
281
  command.extra['cmd_string'] = msg_hash['cmd_string']
281
282
  command.extra['username'] = msg_hash['username']
283
+ # Add approver info if this was a critical command that was approved
284
+ if critical_model
285
+ command.extra['approver'] = critical_model.approver
286
+ end
282
287
  hazardous, hazardous_description = System.commands.cmd_pkt_hazardous?(command)
283
288
 
284
289
  # Initial Are you sure? Hazardous check
@@ -638,7 +638,8 @@ module OpenC3
638
638
  system = System.new([@name], temp_dir)
639
639
  if variables["xtce_output"]
640
640
  puts "Converting target #{@name} to .xtce files in #{variables["xtce_output"]}/#{@name}"
641
- system.packet_config.to_xtce(variables["xtce_output"])
641
+ puts " Using mnemonic '#{variables['time_association_name']}' as the packet time item."
642
+ system.packet_config.to_xtce(variables["xtce_output"], variables['time_association_name'])
642
643
  end
643
644
  unless validate_only
644
645
  build_target_archive(temp_dir, target_folder)
@@ -433,6 +433,9 @@ module OpenC3
433
433
  previous_item = nil
434
434
  warnings = []
435
435
  @sorted_items.each do |item|
436
+ # Skip items with a parent_item since those are accessor-based items within a structure
437
+ # (e.g., JSON, CBOR) that don't have meaningful bit positions - they share the parent's bit_offset
438
+ next if item.parent_item
436
439
  if expected_next_offset and (item.bit_offset < expected_next_offset) and !item.overlap
437
440
  msg = "Bit definition overlap at bit offset #{item.bit_offset} for packet #{@target_name} #{@packet_name} items #{item.name} and #{previous_item.name}"
438
441
  Logger.instance.warn(msg)
@@ -331,8 +331,8 @@ module OpenC3
331
331
  end
332
332
  end # def to_config
333
333
 
334
- def to_xtce(output_dir)
335
- XtceConverter.convert(@commands, @telemetry, output_dir)
334
+ def to_xtce(output_dir, time_association_name)
335
+ XtceConverter.convert(@commands, @telemetry, output_dir, time_association_name)
336
336
  end
337
337
 
338
338
  # Add current packet into hash if it exists
@@ -65,6 +65,15 @@ module OpenC3
65
65
  @parser.verify_num_parameters(max_options - 2, max_options, @usage)
66
66
  end
67
67
  @parser.verify_parameter_naming(1) # Item name is the 1st parameter
68
+
69
+ # ARRAY items cannot have brackets in their name because brackets are used
70
+ # for array indexing in the UI and would cause confusion
71
+ if @parser.keyword.include?('ARRAY')
72
+ item_name = @parser.parameters[0]
73
+ if item_name.include?('[') || item_name.include?(']')
74
+ raise @parser.error("ARRAY items cannot have brackets in their name: #{item_name}", @usage)
75
+ end
76
+ end
68
77
  end
69
78
 
70
79
  def create_packet_item(packet, cmd_or_tlm)