openc3 7.0.0.pre.rc3 → 7.0.0

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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/data/config/interface_modifiers.yaml +1 -1
  3. data/data/config/item_modifiers.yaml +18 -6
  4. data/data/config/telemetry.yaml +1 -1
  5. data/lib/openc3/accessors/json_accessor.rb +1 -1
  6. data/lib/openc3/api/tlm_api.rb +3 -3
  7. data/lib/openc3/config/config_parser.rb +4 -4
  8. data/lib/openc3/conversions/conversion.rb +3 -3
  9. data/lib/openc3/core_ext/faraday.rb +4 -0
  10. data/lib/openc3/logs/log_writer.rb +24 -6
  11. data/lib/openc3/logs/packet_log_writer.rb +1 -4
  12. data/lib/openc3/logs/stream_log_pair.rb +11 -4
  13. data/lib/openc3/logs/text_log_writer.rb +1 -4
  14. data/lib/openc3/microservices/interface_microservice.rb +8 -2
  15. data/lib/openc3/microservices/log_microservice.rb +7 -2
  16. data/lib/openc3/microservices/microservice.rb +10 -4
  17. data/lib/openc3/microservices/queue_microservice.rb +3 -0
  18. data/lib/openc3/microservices/scope_cleanup_microservice.rb +116 -1
  19. data/lib/openc3/microservices/text_log_microservice.rb +4 -1
  20. data/lib/openc3/migrations/20260204000000_remove_decom_reducer.rb +2 -0
  21. data/lib/openc3/models/activity_model.rb +15 -3
  22. data/lib/openc3/models/cvt_model.rb +2 -247
  23. data/lib/openc3/models/plugin_store_model.rb +1 -1
  24. data/lib/openc3/models/script_engine_model.rb +1 -1
  25. data/lib/openc3/models/target_model.rb +32 -34
  26. data/lib/openc3/models/tool_model.rb +18 -5
  27. data/lib/openc3/models/trigger_model.rb +1 -1
  28. data/lib/openc3/models/widget_model.rb +1 -2
  29. data/lib/openc3/operators/operator.rb +9 -7
  30. data/lib/openc3/packets/json_packet.rb +2 -0
  31. data/lib/openc3/packets/packet.rb +1 -0
  32. data/lib/openc3/packets/packet_config.rb +28 -12
  33. data/lib/openc3/script/calendar.rb +8 -0
  34. data/lib/openc3/script/script.rb +19 -0
  35. data/lib/openc3/script/storage.rb +6 -6
  36. data/lib/openc3/system/system.rb +6 -6
  37. data/lib/openc3/tools/cmd_tlm_server/interface_thread.rb +0 -2
  38. data/lib/openc3/top_level.rb +15 -63
  39. data/lib/openc3/topics/limits_event_topic.rb +1 -1
  40. data/lib/openc3/utilities/bucket_utilities.rb +3 -1
  41. data/lib/openc3/utilities/cli_generator.rb +7 -0
  42. data/lib/openc3/utilities/cmd_log.rb +1 -1
  43. data/lib/openc3/utilities/local_mode.rb +3 -0
  44. data/lib/openc3/utilities/process_manager.rb +1 -1
  45. data/lib/openc3/utilities/python_proxy.rb +11 -4
  46. data/lib/openc3/utilities/questdb_client.rb +735 -19
  47. data/lib/openc3/utilities/running_script.rb +25 -7
  48. data/lib/openc3/utilities/script.rb +452 -0
  49. data/lib/openc3/utilities/secrets.rb +1 -1
  50. data/lib/openc3/version.rb +5 -5
  51. data/templates/conversion/conversion.py +0 -8
  52. data/templates/conversion/conversion.rb +0 -11
  53. data/templates/tool_angular/package.json +2 -2
  54. data/templates/tool_react/package.json +1 -1
  55. data/templates/tool_svelte/package.json +1 -1
  56. data/templates/tool_vue/package.json +3 -3
  57. data/templates/widget/package.json +2 -2
  58. metadata +16 -2
  59. data/lib/openc3/migrations/20251022000000_remove_unique_id.rb +0 -23
@@ -15,11 +15,10 @@
15
15
  # This file may also be used under the terms of a commercial license
16
16
  # if purchased from OpenC3, Inc.
17
17
 
18
- require 'set'
19
18
  require 'openc3/utilities/store'
20
19
  require 'openc3/utilities/store_queued'
21
20
  require 'openc3/utilities/questdb_client'
22
- require 'openc3/models/target_model'
21
+ # require 'openc3/models/target_model' # Circular require
23
22
 
24
23
  module OpenC3
25
24
  class CvtModel
@@ -126,251 +125,7 @@ module OpenC3
126
125
  end
127
126
 
128
127
  def self.tsdb_lookup(items, start_time:, end_time: nil, scope: $openc3_scope)
129
- tables = {}
130
- names = []
131
- nil_count = 0
132
- # Cache packet definitions to avoid repeated lookups
133
- packet_cache = {}
134
- # Map column names to item type info for decoding
135
- item_types = {}
136
- # Track calculated timestamp items: { position => { source:, format:, table_index: } }
137
- calculated_items = {}
138
- # Track which timestamp columns we need per table
139
- needed_timestamps = {} # { table_index => Set of column names }
140
- current_position = 0
141
-
142
- # Stored timestamp items that need conversion from timestamp_ns to float seconds
143
- stored_timestamp_items = Set.new(['PACKET_TIMESECONDS', 'RECEIVED_TIMESECONDS'])
144
- # Track stored timestamp items: { position => { column:, table_index: } }
145
- stored_timestamp_positions = {}
146
-
147
- items.each do |item|
148
- target_name, packet_name, orig_item_name, value_type, limits = item
149
- # They will all be nil when item is a nil value
150
- # A nil value indicates a value that does not exist as returned by get_tlm_available
151
- if orig_item_name.nil?
152
- # We know PACKET_TIMESECONDS always exists so we can use it to fill in the nil value
153
- names << "PACKET_TIMESECONDS as __nil#{nil_count}"
154
- nil_count += 1
155
- current_position += 1
156
- next
157
- end
158
- table_name = QuestDBClient.sanitize_table_name(target_name, packet_name, scope: scope)
159
- tables[table_name] = 1
160
- index = tables.find_index {|k,v| k == table_name }
161
-
162
- # Check if this is a stored timestamp item (PACKET_TIMESECONDS or RECEIVED_TIMESECONDS)
163
- # These are stored as timestamp_ns columns and need conversion to float seconds on read
164
- if stored_timestamp_items.include?(orig_item_name)
165
- col_name = "T#{index}.#{orig_item_name}"
166
- names << "\"#{col_name}\""
167
- stored_timestamp_positions[current_position] = { column: col_name, table_index: index }
168
- current_position += 1
169
- next
170
- end
171
-
172
- # Check if this is a calculated timestamp item (PACKET_TIMEFORMATTED or RECEIVED_TIMEFORMATTED)
173
- if QuestDBClient::TIMESTAMP_ITEMS.key?(orig_item_name)
174
- ts_info = QuestDBClient::TIMESTAMP_ITEMS[orig_item_name]
175
- calculated_items[current_position] = {
176
- source: ts_info[:source],
177
- format: ts_info[:format],
178
- table_index: index
179
- }
180
- # Track that we need this timestamp column for this table
181
- needed_timestamps[index] ||= Set.new
182
- needed_timestamps[index] << ts_info[:source]
183
- current_position += 1
184
- next
185
- end
186
-
187
- safe_item_name = QuestDBClient.sanitize_column_name(orig_item_name)
188
-
189
- # Look up item type info from packet definition
190
- cache_key = [target_name, packet_name]
191
- unless packet_cache.key?(cache_key)
192
- begin
193
- packet_cache[cache_key] = TargetModel.packet(target_name, packet_name, scope: scope)
194
- rescue RuntimeError
195
- packet_cache[cache_key] = nil
196
- end
197
- end
198
-
199
- packet_def = packet_cache[cache_key]
200
- item_def = nil
201
- if packet_def
202
- packet_def['items']&.each do |pkt_item|
203
- if pkt_item['name'] == orig_item_name
204
- item_def = pkt_item
205
- break
206
- end
207
- end
208
- end
209
-
210
- case value_type
211
- when 'FORMATTED', 'WITH_UNITS'
212
- col_name = "T#{index}.#{safe_item_name}__F"
213
- names << "\"#{col_name}\""
214
- # Formatted values are always strings, no special decoding needed
215
- item_types[col_name] = { 'data_type' => 'STRING', 'array_size' => nil }
216
- when 'CONVERTED'
217
- col_name = "T#{index}.#{safe_item_name}__C"
218
- names << "\"#{col_name}\""
219
- # Converted values may have different types based on read_conversion
220
- if item_def
221
- rc = item_def['read_conversion']
222
- if rc && rc['converted_type']
223
- item_types[col_name] = { 'data_type' => rc['converted_type'], 'array_size' => item_def['array_size'] }
224
- elsif item_def['states']
225
- # State values are strings
226
- item_types[col_name] = { 'data_type' => 'STRING', 'array_size' => nil }
227
- else
228
- item_types[col_name] = { 'data_type' => item_def['data_type'], 'array_size' => item_def['array_size'] }
229
- end
230
- else
231
- item_types[col_name] = { 'data_type' => nil, 'array_size' => nil }
232
- end
233
- else
234
- col_name = "T#{index}.#{safe_item_name}"
235
- names << "\"#{col_name}\""
236
- if item_def
237
- item_types[col_name] = { 'data_type' => item_def['data_type'], 'array_size' => item_def['array_size'] }
238
- else
239
- item_types[col_name] = { 'data_type' => nil, 'array_size' => nil }
240
- end
241
- end
242
- current_position += 1
243
- if limits
244
- names << "\"T#{index}.#{safe_item_name}__L\""
245
- end
246
- end
247
-
248
- # Add needed timestamp columns to the SELECT
249
- # Track which column alias maps to which timestamp source for result processing
250
- # Note: We use underscores in the alias name to avoid needing quotes, which QuestDB includes in returned field names
251
- timestamp_columns = {} # { "T0___ts_timestamp" => { table_index: 0, source: 'timestamp' } }
252
- needed_timestamps.each do |table_index, ts_columns|
253
- ts_columns.each do |ts_col|
254
- alias_name = "T#{table_index}___ts_#{ts_col}"
255
- names << "T#{table_index}.#{ts_col} as #{alias_name}"
256
- timestamp_columns[alias_name] = { table_index: table_index, source: ts_col }
257
- end
258
- end
259
-
260
- # Build the SQL query
261
- query = "SELECT #{names.join(", ")} FROM "
262
- tables.each_with_index do |(table_name, _), index|
263
- if index == 0
264
- query += "#{table_name} as T#{index} "
265
- else
266
- query += "ASOF JOIN #{table_name} as T#{index} "
267
- end
268
- end
269
- query_params = []
270
- if start_time && !end_time
271
- query += "WHERE T0.PACKET_TIMESECONDS < $1 LIMIT -1"
272
- query_params << start_time
273
- elsif start_time && end_time
274
- query += "WHERE T0.PACKET_TIMESECONDS >= $1 AND T0.PACKET_TIMESECONDS < $2"
275
- query_params << start_time
276
- query_params << end_time
277
- end
278
-
279
- retry_count = 0
280
- begin
281
- conn = QuestDBClient.connection
282
- result = conn.exec_params(query, query_params)
283
- if result.nil? or result.ntuples == 0
284
- return {}
285
- else
286
- data = []
287
- # Build up a results set that is an array of arrays
288
- # Each nested array is a set of 2 items: [value, limits state]
289
- # If the item does not have limits the limits state is nil
290
- result.each_with_index do |tuples, row_num|
291
- data[row_num] ||= []
292
- row_index = 0
293
- # Store timestamp values for this row: { "T0.PACKET_TIMESECONDS" => Time, ... }
294
- row_timestamps = {}
295
- tuples.each do |tuple|
296
- col_name = tuple[0]
297
- col_value = tuple[1]
298
- if col_name.include?("__L")
299
- data[row_num][row_index - 1][1] = col_value
300
- elsif col_name =~ /^__nil/
301
- data[row_num][row_index] = [nil, nil]
302
- row_index += 1
303
- elsif col_name =~ /^T(\d+)___ts_(.+)$/
304
- # This is a timestamp column for calculated items (TIMEFORMATTED)
305
- table_idx = $1.to_i
306
- ts_source = $2
307
- row_timestamps["T#{table_idx}.#{ts_source}"] = col_value
308
- elsif col_name.end_with?('.PACKET_TIMESECONDS', '.RECEIVED_TIMESECONDS') || col_name == 'PACKET_TIMESECONDS' || col_name == 'RECEIVED_TIMESECONDS'
309
- # Stored timestamp column - convert from datetime to float seconds
310
- ts_utc = QuestDBClient.pg_timestamp_to_utc(col_value)
311
- seconds_value = QuestDBClient.format_timestamp(ts_utc, :seconds)
312
- data[row_num][row_index] = [seconds_value, nil]
313
- row_index += 1
314
- # Also store for calculated items (TIMEFORMATTED) that may need this
315
- # Normalize key to T{index}.{col} format for consistency
316
- if col_name.include?('.')
317
- row_timestamps[col_name] = col_value
318
- else
319
- row_timestamps["T0.#{col_name}"] = col_value
320
- end
321
- else
322
- # Decode value using item type info
323
- # QuestDB may return column names without table alias prefix
324
- # Try both the raw column name and prefixed versions
325
- type_info = item_types[col_name]
326
- unless type_info
327
- tables.length.times do |i|
328
- prefixed_name = "T#{i}.#{col_name}"
329
- type_info = item_types[prefixed_name]
330
- break if type_info
331
- end
332
- type_info ||= {}
333
- end
334
- decoded_value = QuestDBClient.decode_value(
335
- col_value,
336
- data_type: type_info['data_type'],
337
- array_size: type_info['array_size']
338
- )
339
- data[row_num][row_index] = [decoded_value, nil]
340
- row_index += 1
341
- end
342
- end
343
-
344
- # Insert calculated timestamp items at their positions
345
- # Insert in ascending order so positions remain valid after each insert
346
- calculated_items.keys.sort.each do |position|
347
- calc_info = calculated_items[position]
348
- ts_key = "T#{calc_info[:table_index]}.#{calc_info[:source]}"
349
- ts_value = row_timestamps[ts_key]
350
- ts_utc = QuestDBClient.pg_timestamp_to_utc(ts_value)
351
- calculated_value = QuestDBClient.format_timestamp(ts_utc, calc_info[:format])
352
- data[row_num].insert(position, [calculated_value, nil])
353
- end
354
- end
355
- # If we only have one row then we return a single array
356
- if result.ntuples == 1
357
- data = data[0]
358
- end
359
- return data
360
- end
361
- rescue IOError, PG::Error => e
362
- # Retry the query because various errors can occur that are recoverable
363
- retry_count += 1
364
- if retry_count > 4
365
- # After the 5th retry just raise the error
366
- raise "Error querying TSDB: #{e.message}"
367
- end
368
- Logger.warn("TSDB: Retrying due to error: #{e.message}")
369
- Logger.warn("TSDB: Last query: #{query}") # Log the last query for debugging
370
- QuestDBClient.disconnect
371
- sleep 0.1
372
- retry
373
- end
128
+ QuestDBClient.tsdb_lookup(items, start_time: start_time, end_time: end_time, scope: scope)
374
129
  end
375
130
 
376
131
  # Return all item values and limit state from the CVT
@@ -18,7 +18,7 @@ module OpenC3
18
18
  class PluginStoreModel < Model
19
19
  PRIMARY_KEY = 'openc3_plugin_store'
20
20
  DEFAULT_STORE_URL = 'https://store.openc3.com'
21
- JSON_ENDPOINT = '/api/v1.1/cosmos_plugins'
21
+ JSON_ENDPOINT = '/api/v1.2/cosmos_plugins'
22
22
 
23
23
  def self.set(plugin_store_data)
24
24
  Store.set(PRIMARY_KEY, plugin_store_data)
@@ -13,7 +13,7 @@
13
13
 
14
14
  require 'openc3/top_level'
15
15
  require 'openc3/models/model'
16
- require 'openc3/models/scope_model'
16
+ # require 'openc3/models/scope_model' # Circular require
17
17
  require 'openc3/utilities/bucket'
18
18
  require 'openc3/utilities/bucket_utilities'
19
19
 
@@ -164,36 +164,41 @@ module OpenC3
164
164
  if target_name.include?('..') || target_name.include?('/') || target_name.include?('\\')
165
165
  raise ArgumentError, "Invalid target_name: #{target_name.inspect}"
166
166
  end
167
- tmp_dir = Dir.mktmpdir
168
- zip_filename = File.join(tmp_dir, "#{target_name}.zip")
169
- Zip.continue_on_exists_proc = true
170
- zip = Zip::File.open(zip_filename, create: true)
167
+ temp_dir = Dir.mktmpdir
168
+ begin
169
+ zip_filename = OpenC3.sanitize_path(File.join(temp_dir, "#{target_name}.zip"))
170
+ Zip.continue_on_exists_proc = true
171
+ zip = Zip::File.open(zip_filename, create: true)
171
172
 
172
- if ENV['OPENC3_LOCAL_MODE']
173
- OpenC3::LocalMode.zip_target(target_name, zip, scope: scope)
174
- else
175
- bucket = Bucket.getClient()
176
- # The trailing slash is important!
177
- prefix = "#{scope}/targets_modified/#{target_name}/"
178
- resp = bucket.list_objects(
179
- bucket: ENV['OPENC3_CONFIG_BUCKET'],
180
- prefix: prefix,
181
- )
182
- resp.each do |item|
183
- # item.key looks like DEFAULT/targets_modified/INST/screens/blah.txt
184
- base_path = item.key.sub(prefix, '') # remove prefix
185
- local_path = File.join(tmp_dir, base_path)
186
- # Ensure dir structure exists, get_object fails if not
187
- FileUtils.mkdir_p(File.dirname(local_path))
188
- bucket.get_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: item.key, path: local_path)
189
- zip.add(base_path, local_path)
173
+ if ENV['OPENC3_LOCAL_MODE']
174
+ OpenC3::LocalMode.zip_target(target_name, zip, scope: scope)
175
+ else
176
+ bucket = Bucket.getClient()
177
+ # The trailing slash is important!
178
+ prefix = "#{scope}/targets_modified/#{target_name}/"
179
+ resp = bucket.list_objects(
180
+ bucket: ENV['OPENC3_CONFIG_BUCKET'],
181
+ prefix: prefix,
182
+ )
183
+ resp.each do |item|
184
+ # item.key looks like DEFAULT/targets_modified/INST/screens/blah.txt
185
+ base_path = item.key.sub(prefix, '') # remove prefix
186
+ local_path = File.join(temp_dir, base_path)
187
+ # Ensure dir structure exists, get_object fails if not
188
+ FileUtils.mkdir_p(File.dirname(local_path))
189
+ bucket.get_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: item.key, path: local_path)
190
+ zip.add(base_path, local_path)
191
+ end
190
192
  end
193
+ zip.close
194
+
195
+ result = OpenStruct.new
196
+ result.filename = File.basename(zip_filename)
197
+ result.contents = File.read(zip_filename, mode: 'rb')
198
+ ensure
199
+ FileUtils.remove_entry_secure(temp_dir, true)
191
200
  end
192
- zip.close
193
201
 
194
- result = OpenStruct.new
195
- result.filename = File.basename(zip_filename)
196
- result.contents = File.read(zip_filename, mode: 'rb')
197
202
  return result
198
203
  end
199
204
 
@@ -393,14 +398,7 @@ module OpenC3
393
398
  shard: 0,
394
399
  scope:
395
400
  )
396
- super("#{scope}__#{PRIMARY_KEY}", name: name, plugin: plugin, updated_at: updated_at,
397
- cmd_buffer_depth: cmd_buffer_depth, cmd_log_cycle_time: cmd_log_cycle_time, cmd_log_cycle_size: cmd_log_cycle_size,
398
- cmd_log_retain_time: cmd_log_retain_time,
399
- tlm_buffer_depth: tlm_buffer_depth, tlm_log_cycle_time: tlm_log_cycle_time, tlm_log_cycle_size: tlm_log_cycle_size,
400
- tlm_log_retain_time: tlm_log_retain_time,
401
- cmd_decom_retain_time: cmd_decom_retain_time, tlm_decom_retain_time: tlm_decom_retain_time,
402
- cleanup_poll_time: cleanup_poll_time, needs_dependencies: needs_dependencies, target_microservices: target_microservices,
403
- scope: scope)
401
+ super("#{scope}__#{PRIMARY_KEY}", name: name, plugin: plugin, updated_at: updated_at, scope: scope)
404
402
  @folder_name = folder_name
405
403
  @requires = requires
406
404
  @ignored_parameters = ignored_parameters
@@ -16,7 +16,7 @@
16
16
  # if purchased from OpenC3, Inc.
17
17
 
18
18
  require 'openc3/models/model'
19
- require 'openc3/models/scope_model'
19
+ # require 'openc3/models/scope_model' # Circular require
20
20
  require 'openc3/utilities/bucket'
21
21
  require 'openc3/utilities/bucket_utilities'
22
22
  require 'rack'
@@ -179,7 +179,7 @@ module OpenC3
179
179
  end
180
180
  end
181
181
 
182
- if @url and !@url.start_with?('/') and @url !~ URI::regexp
182
+ if @url and !@url.start_with?('/') and @url !~ URI::RFC2396_PARSER.make_regexp
183
183
  raise "URL must be a full URL (http://domain.com/path) or a relative path (/path)"
184
184
  end
185
185
 
@@ -250,9 +250,22 @@ module OpenC3
250
250
 
251
251
  variables["tool_name"] = @name
252
252
  start_path = "/tools/#{@folder_name}/"
253
- Dir.glob(gem_path + start_path + "**/*") do |filename|
254
- next if filename == '.' or filename == '..' or File.directory?(filename)
255
-
253
+ # Sort files so dependencies are uploaded before dependents:
254
+ # fonts first, then CSS, then index.html last (it triggers all other loads)
255
+ filenames = Dir.glob(gem_path + start_path + "**/*")
256
+ filenames.reject! { |f| f == '.' or f == '..' or File.directory?(f) }
257
+ filenames.sort_by! do |filename|
258
+ if filename.include?('/fonts/')
259
+ [0, filename]
260
+ elsif filename.include?('/css/')
261
+ [1, filename]
262
+ elsif File.basename(filename) == 'index.html'
263
+ [3, filename]
264
+ else
265
+ [2, filename]
266
+ end
267
+ end
268
+ filenames.each do |filename|
256
269
  key = filename.split(gem_path + '/tools/')[-1]
257
270
  extension = filename.split('.')[-1]
258
271
  content_type = Rack::Mime.mime_type(".#{extension}")
@@ -19,7 +19,7 @@ require 'openc3/models/model'
19
19
  require 'openc3/models/microservice_model'
20
20
  require 'openc3/models/target_model'
21
21
  require 'openc3/models/trigger_group_model'
22
- require 'openc3/models/reaction_model'
22
+ # require 'openc3/models/reaction_model' # Remove circular require
23
23
  require 'openc3/topics/autonomic_topic'
24
24
 
25
25
  module OpenC3
@@ -15,9 +15,8 @@
15
15
  # This file may also be used under the terms of a commercial license
16
16
  # if purchased from OpenC3, Inc.
17
17
 
18
- require 'openc3/top_level'
19
18
  require 'openc3/models/model'
20
- require 'openc3/models/scope_model'
19
+ # require 'openc3/models/scope_model' # Circular require
21
20
  require 'openc3/utilities/bucket'
22
21
  require 'openc3/utilities/bucket_utilities'
23
22
 
@@ -54,8 +54,10 @@ module OpenC3
54
54
  end
55
55
 
56
56
  def finalize
57
- extract()
58
- close()
57
+ unless closed?
58
+ extract()
59
+ close()
60
+ end
59
61
  unlink()
60
62
 
61
63
  output = ''
@@ -174,7 +176,7 @@ module OpenC3
174
176
  end
175
177
  @process.stop
176
178
  end
177
- FileUtils.remove_entry_secure(@temp_dir, true)
179
+ FileUtils.remove_entry_secure(@temp_dir, true) if @temp_dir
178
180
  @process = nil
179
181
  end
180
182
 
@@ -300,6 +302,7 @@ module OpenC3
300
302
  # Respawn process
301
303
  output = p.extract_output
302
304
  Logger.error("Unexpected process died... respawning! #{p.cmd_line}\n#{output}\n", scope: p.scope)
305
+ p.hard_stop
303
306
  p.start
304
307
  end
305
308
  end
@@ -308,6 +311,7 @@ module OpenC3
308
311
 
309
312
  def shutdown_processes(processes)
310
313
  # Make a copy so we don't mutate original
314
+ hard_stop_processes = processes.dup
311
315
  processes = processes.dup
312
316
 
313
317
  Logger.info("Commanding soft stops...")
@@ -331,10 +335,8 @@ module OpenC3
331
335
  end
332
336
  sleep(0.1)
333
337
  end
334
- if processes.length > 0
335
- Logger.debug("Commanding hard stops...")
336
- processes.each { |_name, p| p.hard_stop }
337
- end
338
+ Logger.debug("Commanding hard stops...")
339
+ hard_stop_processes.each { |_name, p| p.output_increment; p.extract_output; p.hard_stop }
338
340
  end
339
341
 
340
342
  def shutdown
@@ -189,6 +189,8 @@ module OpenC3
189
189
  postfix = 'C'
190
190
  when :FORMATTED, :WITH_UNITS
191
191
  postfix = 'F'
192
+ else
193
+ raise "Unsupported value type: #{value_type}"
192
194
  end
193
195
  case reduced_type
194
196
  when :MIN
@@ -16,6 +16,7 @@
16
16
  # if purchased from OpenC3, Inc.
17
17
 
18
18
  require 'digest'
19
+ require 'active_support/core_ext/object/deep_dup'
19
20
  require 'openc3/packets/structure'
20
21
  require 'openc3/packets/packet_item'
21
22
  require 'openc3/ext/packet' if RUBY_ENGINE == 'ruby' and !ENV['OPENC3_NO_EXT']
@@ -252,7 +252,7 @@ module OpenC3
252
252
  #######################################################################
253
253
  when 'STATE', 'READ_CONVERSION', 'WRITE_CONVERSION', 'POLY_READ_CONVERSION',\
254
254
  'POLY_WRITE_CONVERSION', 'SEG_POLY_READ_CONVERSION', 'SEG_POLY_WRITE_CONVERSION',\
255
- 'GENERIC_READ_CONVERSION_START', 'GENERIC_WRITE_CONVERSION_START', 'REQUIRED',\
255
+ 'GENERIC_READ_CONVERSION_START', 'GENERIC_WRITE_CONVERSION_START', 'CONVERTED_DATA', 'REQUIRED',\
256
256
  'LIMITS', 'LIMITS_RESPONSE', 'UNITS', 'FORMAT_STRING', 'DESCRIPTION',\
257
257
  'MINIMUM_VALUE', 'MAXIMUM_VALUE', 'DEFAULT_VALUE', 'OVERFLOW', 'OVERLAP', 'KEY', 'VARIABLE_BIT_SIZE',\
258
258
  'OBFUSCATE'
@@ -675,11 +675,6 @@ module OpenC3
675
675
  klass = OpenC3.require_class(params[0])
676
676
  conversion = klass.new(*params[1..(params.length - 1)])
677
677
  @current_item.public_send("#{keyword.downcase}=".to_sym, conversion)
678
- if klass != ProcessorConversion and (conversion.converted_type.nil? or conversion.converted_bit_size.nil?)
679
- msg = "Read Conversion #{params[0]} on item #{@current_item.name} does not specify converted type or bit size"
680
- @warnings << msg
681
- Logger.instance.warn @warnings[-1]
682
- end
683
678
  else
684
679
  conversion = PythonProxy.new('Conversion', params[0], *params[1..(params.length - 1)])
685
680
  @current_item.public_send("#{keyword.downcase}=".to_sym, conversion)
@@ -719,8 +714,9 @@ module OpenC3
719
714
  # All config.lines following this config.line are considered part
720
715
  # of the conversion until an end of conversion marker is found
721
716
  when 'GENERIC_READ_CONVERSION_START', 'GENERIC_WRITE_CONVERSION_START'
722
- usage = "#{keyword} <Converted Type (optional)> <Converted Bit Size (optional)>"
723
- parser.verify_num_parameters(0, 2, usage)
717
+ # As of COSMOS 7 the converted type and bit size are deprecated
718
+ # but we're still allowing them to be defined as parameters for backward compatibility
719
+ parser.verify_num_parameters(0, 2, keyword)
724
720
  @proc_text = ''
725
721
  @building_generic_conversion = true
726
722
  parser.set_preserve_lines(true)
@@ -731,10 +727,30 @@ module OpenC3
731
727
  raise parser.error("Invalid converted_type: #{@converted_type}.") unless CONVERTED_DATA_TYPES.include? @converted_type
732
728
  end
733
729
  @converted_bit_size = Integer(params[1]) if params[1]
734
- if @converted_type.nil? or @converted_bit_size.nil?
735
- msg = "Generic Conversion on item #{@current_item.name} does not specify converted type or bit size"
736
- @warnings << msg
737
- Logger.instance.warn @warnings[-1]
730
+
731
+ # Define the converted data type, bit size, and optional array size
732
+ # for items with read conversions (especially DERIVED items)
733
+ when 'CONVERTED_DATA'
734
+ usage = "CONVERTED_DATA <Converted Bit Size> <Converted Type> <Converted Array Size (optional)>"
735
+ parser.verify_num_parameters(2, 3, usage)
736
+ raise parser.error("#{keyword} requires a current item") unless @current_item
737
+ raise parser.error("#{keyword} requires a current item with a conversion") unless @current_item.read_conversion or @current_item.write_conversion
738
+ converted_bit_size = Integer(params[0])
739
+ converted_type = params[1].upcase.intern
740
+ raise parser.error("Invalid converted_type: #{converted_type}.") unless CONVERTED_DATA_TYPES.include? converted_type
741
+ if @current_item.read_conversion
742
+ @current_item.read_conversion.converted_type = converted_type
743
+ @current_item.read_conversion.converted_bit_size = converted_bit_size
744
+ if params[2]
745
+ @current_item.read_conversion.converted_array_size = Integer(params[2])
746
+ end
747
+ end
748
+ if @current_item.write_conversion
749
+ @current_item.write_conversion.converted_type = converted_type
750
+ @current_item.write_conversion.converted_bit_size = converted_bit_size
751
+ if params[2]
752
+ @current_item.write_conversion.converted_array_size = Integer(params[2])
753
+ end
738
754
  end
739
755
 
740
756
  # Define a set of limits for the current telemetry item
@@ -52,6 +52,14 @@ module OpenC3
52
52
  return _cal_handle_response(response, 'Failed to delete timeline')
53
53
  end
54
54
 
55
+ # Creates an activity for the specified timeline.
56
+ #
57
+ # @param name [String] The name of the timeline.
58
+ # @param kind [String] The kind of activity. Must be one of "COMMAND", "SCRIPT", or "RESERVE".
59
+ # @param start [DateTime] The start time of the activity.
60
+ # @param stop [DateTime] The stop time of the activity.
61
+ # @param data [Hash, optional] Additional data to associate with the activity. Defaults to {}. Any activity can provide "username", "notes", and "customTitle". "command", "script", and "reserve" keys are reserves for the corresponding activity kind, with "environment" also available for script activities.
62
+ # @param scope [String, optional] The scope of the activity. Defaults to OPENC3_SCOPE, must correspond to the timeline.
55
63
  def create_timeline_activity(name, kind:, start:, stop:, data: {}, scope: $openc3_scope)
56
64
  kind = kind.to_s.downcase()
57
65
  kinds = %w(command script reserve)
@@ -47,6 +47,9 @@ $disconnect = false
47
47
  $openc3_scope = ENV['OPENC3_SCOPE'] || 'DEFAULT'
48
48
  $openc3_in_cluster = false
49
49
 
50
+ saved_verbose = $VERBOSE
51
+ $VERBOSE = false
52
+
50
53
  module OpenC3
51
54
  module Script
52
55
  private
@@ -177,6 +180,10 @@ module OpenC3
177
180
  message_box(string, *items, **options)
178
181
  end
179
182
 
183
+ def check_box(string, *items, **options)
184
+ message_box(string, *items, **options)
185
+ end
186
+
180
187
  def _file_dialog(title, message, filter:)
181
188
  answer = ''
182
189
  path = "./*"
@@ -199,6 +206,16 @@ module OpenC3
199
206
  _file_dialog(title, message, filter)
200
207
  end
201
208
 
209
+ def open_bucket_dialog(title, message = "Open Bucket File")
210
+ answer = ''
211
+ while answer.empty?
212
+ print "#{title}\n#{message}\n<Type bucket file path (e.g. BUCKET/path/to/file)>:"
213
+ answer = gets
214
+ answer.chomp!
215
+ end
216
+ return answer
217
+ end
218
+
202
219
  def prompt(string, text_color: nil, background_color: nil, font_size: nil, font_family: nil, details: nil)
203
220
  print "#{string}: "
204
221
  print "Details: #{details}\n" if details
@@ -363,3 +380,5 @@ module OpenC3
363
380
  end
364
381
  end
365
382
  end
383
+
384
+ $VERBOSE = saved_verbose