openc3 6.10.1 → 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: 0b9dccfc06d5de8e5605e77c5590365bebcbe339fb63fa61402cfa3d55834961
4
- data.tar.gz: 45927a89e2ff628b31c0c153b57a7a177560f2ca00a3f76af70f981af1899d8a
3
+ metadata.gz: c1459e2b7d8160f09840d33a7fc6aa6314eb34367d68f41d45902a0203b37f57
4
+ data.tar.gz: 6de6d85032257c4e19f9470846056d5cf4aa8acc0498e4f9691543598a12856f
5
5
  SHA512:
6
- metadata.gz: 4e8b6f535c5015a123bf92c4b8b2a437008b9eebb27f56693f291215785d35fef59d45bf89d04d561fd38c399ecdceb254d9b963161d8ab587aeba16ae15d935
7
- data.tar.gz: 9b9f097230caa46397699dbc145740023190028d3cdf4355882a53fbedbc04988283d3634d1027c3e334f68b38d8d9bb961ced95d537e10d0e678f124f960318
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}"
@@ -100,6 +100,7 @@ STRUCTURE:
100
100
  modifiers:
101
101
  <%= MetaConfigParser.load('parameter_modifiers.yaml').to_meta_config_yaml(4) %>
102
102
  summary: Adds and flattens a structure (generally a virtual packet) into the current packet. The specific named item is BLOCK type and hidden.
103
+ since: 6.10.0
103
104
  parameters:
104
105
  - name: Name
105
106
  required: true
@@ -134,6 +135,7 @@ APPEND_STRUCTURE:
134
135
  modifiers:
135
136
  <%= MetaConfigParser.load('parameter_modifiers.yaml').to_meta_config_yaml(4) %>
136
137
  summary: Adds and flattens a structure (generally a virtual packet) into the current packet. The specific named item is BLOCK type and hidden.
138
+ since: 6.10.0
137
139
  parameters:
138
140
  - name: Name
139
141
  required: true
@@ -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
@@ -92,7 +92,8 @@ WIDGET:
92
92
  SCRIPT_ENGINE:
93
93
  summary: Define a script engine to add language support to Script Runner
94
94
  example: SCRIPT_ENGINE .print print_script_engine.py
95
- description: Defines a script engine to add language support to Script Runner
95
+ description: Defines a script engine to add language support to Script Runner. For a realistic example, see our [CSTOL](https://github.com/OpenC3/openc3-cosmos-script-engine-cstol) plugin.
96
+ since: 6.5.0
96
97
  parameters:
97
98
  - name: Extension
98
99
  description: Extension that will use this script engine
@@ -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 &lt;%= target_name %&gt;.
19
+ See [ERB target_name](/docs/configuration/format#target_name) for more information.
15
20
  values: .*
16
21
  modifiers:
17
22
  CMD_BUFFER_DEPTH:
@@ -91,13 +91,13 @@ CMD_UNIQUE_ID_MODE:
91
91
  Ideally all commands for a target are identified using the exact same bit offset, size,
92
92
  and type field in each command. If ANY command identifiers differ then this flag must be set
93
93
  to force a brute force identification method.
94
- warning: Using this mode significantly slows packet identification
95
94
  since: 4.4.0
95
+ deprecated: Since 6.10.0 this condition is now automatically detected
96
96
  TLM_UNIQUE_ID_MODE:
97
97
  summary: Telemetry packets identifiers don't all share the same bit offset, size, and type
98
98
  description:
99
99
  Ideally all telemetry for a target are identified using the exact same bit offset, size,
100
100
  and type field in each packet. If ANY telemetry identifiers differ then this flag must be set
101
101
  to force a brute force identification method.
102
- warning: Using this mode significantly slows packet identification
103
102
  since: 4.4.0
103
+ deprecated: Since 6.10.0 this condition is now automatically detected
@@ -90,6 +90,7 @@ STRUCTURE:
90
90
  modifiers:
91
91
  <%= MetaConfigParser.load('item_modifiers.yaml').to_meta_config_yaml(4) %>
92
92
  summary: Adds and flattens a structure (generally a virtual packet) into the current packet. The specific named item is BLOCK type and hidden.
93
+ since: 6.10.0
93
94
  parameters:
94
95
  - name: Name
95
96
  required: true
@@ -124,6 +125,7 @@ APPEND_STRUCTURE:
124
125
  modifiers:
125
126
  <%= MetaConfigParser.load('item_modifiers.yaml').to_meta_config_yaml(4) %>
126
127
  summary: Adds and flattens a structure (generally a virtual packet) into the current packet. The specific named item is BLOCK type and hidden.
128
+ since: 6.10.0
127
129
  parameters:
128
130
  - name: Name
129
131
  required: true
@@ -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
 
@@ -22,6 +22,7 @@
22
22
 
23
23
  require 'openc3/packets/binary_accessor'
24
24
  require 'openc3/ext/string' if RUBY_ENGINE == 'ruby' and !ENV['OPENC3_NO_EXT']
25
+ require 'yaml'
25
26
 
26
27
  # OpenC3 specific additions to the Ruby String class
27
28
  class String
@@ -40,6 +41,8 @@ class String
40
41
  HEX_CHECK_REGEX = /\A\s*0[xX][\dabcdefABCDEF]+\s*\z/
41
42
  # Regular expression to identify a String as an Array of numbers
42
43
  ARRAY_CHECK_REGEX = /\A\s*\[.*\]\s*\z/
44
+ # Regular expression to identify a String containing object notation
45
+ OBJECT_CHECK_REGEX = /\A\s*\{.*\}\s*\z/
43
46
 
44
47
  # Displays a String containing binary data in a human readable format by
45
48
  # converting each byte to the hex representation.
@@ -209,6 +212,11 @@ class String
209
212
  if ARRAY_CHECK_REGEX.match?(self) then true else false end
210
213
  end
211
214
 
215
+ # @return [Boolean] Whether the String represents an Object
216
+ def is_object?
217
+ if OBJECT_CHECK_REGEX.match?(self) then true else false end
218
+ end
219
+
212
220
  # @return [Boolean] Whether the string contains only printable characters
213
221
  def is_printable?
214
222
  if NON_PRINTABLE_REGEX.match?(self) then false else true end
@@ -238,9 +246,9 @@ class String
238
246
  elsif self.is_hex?
239
247
  # Hex
240
248
  return_value = Integer(self)
241
- elsif self.is_array?
242
- # Array
243
- return_value = eval(self)
249
+ elsif self.is_array? or self.is_object?
250
+ # Array or Object
251
+ return_value = YAML.safe_load(self)
244
252
  end
245
253
  rescue Exception
246
254
  # Something went wrong so just return the string as is
@@ -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
@@ -55,6 +55,7 @@ module OpenC3
55
55
  MicroserviceStatusModel.set(microservice.as_json(), scope: microservice.scope)
56
56
  microservice.state = 'RUNNING'
57
57
  microservice.run
58
+ Logger.info("Microservice #{name} run method returned cleanly and will now shutdown.")
58
59
  microservice.state = 'FINISHED'
59
60
  rescue Exception => e
60
61
  if SystemExit === e or SignalException === e
@@ -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)
@@ -905,6 +908,11 @@ module OpenC3
905
908
  write_item(item, item.structure.buffer(false), :RAW, buffer)
906
909
  end
907
910
  elsif not item.default.nil? and not item.parent_item
911
+ # Skip writing default for accessor-based items when template is used
912
+ # The template already contains the correct default value
913
+ # Only skip if key is explicitly set (different from item name) - this distinguishes
914
+ # JsonAccessor (key="$.field") from TemplateAccessor (key=name="FIELD")
915
+ next if item.key and item.key != item.name and @template and use_template
908
916
  write_item(item, item.default, :CONVERTED, buffer) unless skip_item_names and upcase_skip_item_names.include?(item.name)
909
917
  end
910
918
  end
@@ -1233,7 +1241,20 @@ module OpenC3
1233
1241
  # Items with derived items last
1234
1242
  @sorted_items.each do |item|
1235
1243
  if item.data_type != :DERIVED
1236
- items << item.as_json(*a)
1244
+ item_hash = item.as_json(*a)
1245
+ # For accessor-based items with a template, extract the default from the template
1246
+ # Only extract for items with explicit keys (different from item name) - this distinguishes
1247
+ # JsonAccessor (key="$.field") from TemplateAccessor (key=name="FIELD")
1248
+ if item.key and item.key != item.name and @template
1249
+ begin
1250
+ template_value = read_item_from_template(item)
1251
+ item_hash['default'] = template_value unless template_value.nil?
1252
+ rescue => e
1253
+ # If we can't read from template, keep the original default
1254
+ Logger.debug("Could not read template default for #{@target_name} #{@packet_name} #{item.name}: #{e.message}")
1255
+ end
1256
+ end
1257
+ items << item_hash
1237
1258
  end
1238
1259
  end
1239
1260
  @sorted_items.each do |item|
@@ -1358,6 +1379,56 @@ module OpenC3
1358
1379
 
1359
1380
  protected
1360
1381
 
1382
+ # Read item value from template, handling PythonProxy accessors
1383
+ # For PythonProxy, parse template directly since the proxy's class method returns a string
1384
+ def read_item_from_template(item)
1385
+ accessor_class = @accessor.class
1386
+
1387
+ # For PythonProxy, accessor.class returns the class name as a string
1388
+ # We need to parse the template directly based on the accessor type
1389
+ if accessor_class.is_a?(String)
1390
+ case accessor_class
1391
+ when 'JsonAccessor'
1392
+ return read_json_template_item(item)
1393
+ when 'CborAccessor'
1394
+ return read_cbor_template_item(item)
1395
+ when 'XmlAccessor'
1396
+ return read_xml_template_item(item)
1397
+ else
1398
+ # Unknown accessor type - can't read from template
1399
+ return nil
1400
+ end
1401
+ else
1402
+ # Normal accessor - use the class method
1403
+ return accessor_class.read_item(item, @template)
1404
+ end
1405
+ end
1406
+
1407
+ # Parse JSON template and extract item value using JSONPath key
1408
+ def read_json_template_item(item)
1409
+ require 'json'
1410
+ require 'jsonpath'
1411
+ json_data = JSON.parse(@template.to_s, allow_nan: true, create_additions: true)
1412
+ JsonPath.new(item.key).first(json_data)
1413
+ end
1414
+
1415
+ # Parse CBOR template and extract item value using JSONPath key
1416
+ def read_cbor_template_item(item)
1417
+ require 'cbor'
1418
+ require 'jsonpath'
1419
+ cbor_data = CBOR.decode(@template.to_s)
1420
+ JsonPath.new(item.key).first(cbor_data)
1421
+ end
1422
+
1423
+ # Parse XML template and extract item value using XPath key
1424
+ def read_xml_template_item(item)
1425
+ require 'nokogiri'
1426
+ doc = Nokogiri::XML(@template.to_s)
1427
+ node = doc.xpath(item.key).first
1428
+ return nil unless node
1429
+ node.text
1430
+ end
1431
+
1361
1432
  def handle_limits_states(item, value)
1362
1433
  # Retrieve limits state for the given value
1363
1434
  limits_state = item.state_colors[value]
@@ -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)