openc3 7.0.0.pre.rc3 → 7.0.1

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +58 -10
  3. data/bin/pipinstall +38 -6
  4. data/data/config/command_modifiers.yaml +1 -0
  5. data/data/config/interface_modifiers.yaml +1 -1
  6. data/data/config/item_modifiers.yaml +20 -7
  7. data/data/config/table_parameter_modifiers.yaml +3 -1
  8. data/data/config/telemetry.yaml +1 -1
  9. data/lib/openc3/accessors/json_accessor.rb +1 -1
  10. data/lib/openc3/accessors/template_accessor.rb +9 -0
  11. data/lib/openc3/api/tlm_api.rb +3 -3
  12. data/lib/openc3/config/config_parser.rb +4 -4
  13. data/lib/openc3/conversions/conversion.rb +3 -3
  14. data/lib/openc3/core_ext/faraday.rb +4 -0
  15. data/lib/openc3/interfaces/interface.rb +1 -6
  16. data/lib/openc3/logs/log_writer.rb +24 -6
  17. data/lib/openc3/logs/packet_log_writer.rb +1 -4
  18. data/lib/openc3/logs/stream_log_pair.rb +11 -4
  19. data/lib/openc3/logs/text_log_writer.rb +1 -4
  20. data/lib/openc3/microservices/decom_microservice.rb +1 -1
  21. data/lib/openc3/microservices/interface_decom_common.rb +22 -8
  22. data/lib/openc3/microservices/interface_microservice.rb +14 -3
  23. data/lib/openc3/microservices/log_microservice.rb +7 -2
  24. data/lib/openc3/microservices/microservice.rb +10 -4
  25. data/lib/openc3/microservices/queue_microservice.rb +3 -0
  26. data/lib/openc3/microservices/scope_cleanup_microservice.rb +116 -1
  27. data/lib/openc3/microservices/text_log_microservice.rb +4 -1
  28. data/lib/openc3/migrations/20260204000000_remove_decom_reducer.rb +2 -0
  29. data/lib/openc3/models/activity_model.rb +15 -3
  30. data/lib/openc3/models/cvt_model.rb +2 -247
  31. data/lib/openc3/models/plugin_model.rb +9 -1
  32. data/lib/openc3/models/plugin_store_model.rb +1 -1
  33. data/lib/openc3/models/python_package_model.rb +1 -1
  34. data/lib/openc3/models/reaction_model.rb +27 -9
  35. data/lib/openc3/models/script_engine_model.rb +1 -1
  36. data/lib/openc3/models/target_model.rb +32 -34
  37. data/lib/openc3/models/tool_model.rb +18 -5
  38. data/lib/openc3/models/trigger_model.rb +25 -8
  39. data/lib/openc3/models/widget_model.rb +1 -2
  40. data/lib/openc3/operators/operator.rb +9 -7
  41. data/lib/openc3/packets/json_packet.rb +2 -0
  42. data/lib/openc3/packets/packet.rb +1 -0
  43. data/lib/openc3/packets/packet_config.rb +28 -12
  44. data/lib/openc3/script/api_shared.rb +39 -2
  45. data/lib/openc3/script/calendar.rb +40 -10
  46. data/lib/openc3/script/extract.rb +46 -13
  47. data/lib/openc3/script/script.rb +19 -0
  48. data/lib/openc3/script/storage.rb +6 -6
  49. data/lib/openc3/system/system.rb +6 -6
  50. data/lib/openc3/tools/cmd_tlm_server/interface_thread.rb +0 -2
  51. data/lib/openc3/top_level.rb +15 -63
  52. data/lib/openc3/topics/decom_interface_topic.rb +19 -4
  53. data/lib/openc3/topics/interface_topic.rb +21 -2
  54. data/lib/openc3/topics/limits_event_topic.rb +1 -1
  55. data/lib/openc3/utilities/bucket_utilities.rb +3 -1
  56. data/lib/openc3/utilities/cli_generator.rb +7 -0
  57. data/lib/openc3/utilities/cmd_log.rb +1 -1
  58. data/lib/openc3/utilities/ctrf.rb +231 -0
  59. data/lib/openc3/utilities/local_mode.rb +3 -0
  60. data/lib/openc3/utilities/process_manager.rb +1 -1
  61. data/lib/openc3/utilities/python_proxy.rb +11 -4
  62. data/lib/openc3/utilities/questdb_client.rb +739 -22
  63. data/lib/openc3/utilities/running_script.rb +25 -7
  64. data/lib/openc3/utilities/script.rb +452 -0
  65. data/lib/openc3/utilities/secrets.rb +1 -1
  66. data/lib/openc3/version.rb +6 -6
  67. data/templates/conversion/conversion.py +0 -8
  68. data/templates/conversion/conversion.rb +0 -11
  69. data/templates/tool_angular/package.json +2 -2
  70. data/templates/tool_react/package.json +1 -1
  71. data/templates/tool_svelte/package.json +1 -1
  72. data/templates/tool_vue/package.json +3 -4
  73. data/templates/widget/package.json +2 -2
  74. metadata +17 -2
  75. data/lib/openc3/migrations/20251022000000_remove_unique_id.rb +0 -23
@@ -32,10 +32,12 @@ module OpenC3
32
32
  ACTION_TYPES = [SCRIPT_REACTION, COMMAND_REACTION, NOTIFY_REACTION]
33
33
 
34
34
  def self.create_unique_name(scope:)
35
- reaction_names = self.names(scope: scope) # comes back sorted
35
+ reaction_names = self.names(scope: scope)
36
36
  num = 1 # Users count with 1
37
- if reaction_names[-1]
38
- num = reaction_names[-1][5..-1].to_i + 1
37
+ unless reaction_names.empty?
38
+ # Extract numeric suffixes and find the max to avoid lexicographic sort issues
39
+ max_num = reaction_names.map { |name| name[5..-1].to_i }.max
40
+ num = max_num + 1
39
41
  end
40
42
  return "REACT#{num}"
41
43
  end
@@ -77,7 +79,7 @@ module OpenC3
77
79
  end
78
80
 
79
81
  attr_reader :name, :scope, :snooze, :triggers, :actions, :enabled, :trigger_level, :snoozed_until
80
- attr_accessor :username, :shard
82
+ attr_accessor :username, :shard, :label
81
83
 
82
84
  def initialize(
83
85
  name:,
@@ -90,6 +92,7 @@ module OpenC3
90
92
  snoozed_until: nil,
91
93
  username: nil,
92
94
  shard: 0,
95
+ label: nil,
93
96
  updated_at: nil
94
97
  )
95
98
  super("#{scope}#{PRIMARY_KEY}", name: name, scope: scope)
@@ -102,6 +105,7 @@ module OpenC3
102
105
  @triggers = validate_triggers(triggers)
103
106
  @username = username
104
107
  @shard = shard.to_i # to_i to handle nil
108
+ @label = label
105
109
  @updated_at = updated_at
106
110
  end
107
111
 
@@ -177,28 +181,40 @@ module OpenC3
177
181
  return actions
178
182
  end
179
183
 
180
- def verify_triggers
184
+ # Validate that all triggers exist, but do not persist dependent changes yet.
185
+ # Returns the list of trigger models that need updating.
186
+ def validate_triggers_exist
181
187
  if @triggers.empty?
182
188
  raise ReactionInputError.new "reaction must contain at least one valid trigger: #{@triggers}"
183
189
  end
184
190
 
191
+ models_to_update = []
185
192
  @triggers.each do | trigger |
186
193
  model = TriggerModel.get(name: trigger['name'], group: trigger['group'], scope: @scope)
187
194
  if model.nil?
188
195
  raise ReactionInputError.new "failed to find trigger: #{trigger}"
189
196
  end
190
- model.update_dependents(dependent: @name)
191
- model.update()
197
+ unless model.dependents.include?(@name)
198
+ model.update_dependents(dependent: @name)
199
+ models_to_update << model
200
+ end
192
201
  end
202
+ models_to_update
203
+ end
204
+
205
+ # Persist dependent changes to trigger models
206
+ def commit_trigger_dependents(models)
207
+ models.each { |model| model.update() }
193
208
  end
194
209
 
195
210
  def create
196
211
  unless Store.hget(@primary_key, @name).nil?
197
212
  raise ReactionInputError.new "existing reaction found: #{@name}"
198
213
  end
199
- verify_triggers()
214
+ models = validate_triggers_exist()
200
215
  @updated_at = Time.now.to_nsec_from_epoch
201
216
  Store.hset(@primary_key, @name, JSON.generate(as_json, allow_nan: true))
217
+ commit_trigger_dependents(models)
202
218
  notify(kind: 'created')
203
219
  end
204
220
 
@@ -222,9 +238,10 @@ module OpenC3
222
238
  end
223
239
  end
224
240
 
225
- verify_triggers()
241
+ models = validate_triggers_exist()
226
242
  @updated_at = Time.now.to_nsec_from_epoch
227
243
  Store.hset(@primary_key, @name, JSON.generate(as_json, allow_nan: true))
244
+ commit_trigger_dependents(models)
228
245
  # No notification as this is only called via reaction_controller which already notifies
229
246
  end
230
247
 
@@ -281,6 +298,7 @@ module OpenC3
281
298
  'actions' => @actions,
282
299
  'username' => @username,
283
300
  'shard' => @shard,
301
+ 'label' => @label,
284
302
  'updated_at' => @updated_at
285
303
  }
286
304
  end
@@ -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
@@ -52,10 +52,12 @@ module OpenC3
52
52
  TRIGGER_TYPE = 'trigger'.freeze
53
53
 
54
54
  def self.create_unique_name(group:, scope:)
55
- trigger_names = self.names(group: group, scope: scope) # comes back sorted
55
+ trigger_names = self.names(group: group, scope: scope)
56
56
  num = 1 # Users count with 1
57
- if trigger_names[-1]
58
- num = trigger_names[-1][4..-1].to_i + 1
57
+ unless trigger_names.empty?
58
+ # Extract numeric suffixes and find the max to avoid lexicographic sort issues
59
+ max_num = trigger_names.map { |name| name[4..-1].to_i }.max
60
+ num = max_num + 1
59
61
  end
60
62
  return "TRIG#{num}"
61
63
  end
@@ -97,6 +99,7 @@ module OpenC3
97
99
  end
98
100
 
99
101
  attr_reader :name, :scope, :state, :group, :enabled, :left, :operator, :right, :dependents, :roots
102
+ attr_accessor :label
100
103
 
101
104
  def initialize(
102
105
  name:,
@@ -108,6 +111,7 @@ module OpenC3
108
111
  state: false,
109
112
  enabled: true,
110
113
  dependents: nil,
114
+ label: nil,
111
115
  updated_at: nil
112
116
  )
113
117
  super("#{scope}#{PRIMARY_KEY}#{group}", name: name, scope: scope)
@@ -119,6 +123,7 @@ module OpenC3
119
123
  @operator = validate_operator(operator: operator)
120
124
  @right = validate_operand(operand: right, right: true)
121
125
  @dependents = dependents
126
+ @label = label
122
127
  @updated_at = updated_at
123
128
  selected_group = TriggerGroupModel.get(name: @group, scope: @scope)
124
129
  if selected_group.nil?
@@ -175,8 +180,11 @@ module OpenC3
175
180
  end
176
181
  end
177
182
 
178
- def verify_triggers
183
+ # Validate that all root triggers exist, but do not persist dependent changes yet.
184
+ # Returns the list of root trigger models that need updating.
185
+ def validate_roots
179
186
  @dependents = [] if @dependents.nil?
187
+ models_to_update = []
180
188
  @roots.each do | trigger |
181
189
  model = TriggerModel.get(name: trigger, group: @group, scope: @scope)
182
190
  if model.nil?
@@ -184,25 +192,33 @@ module OpenC3
184
192
  end
185
193
  unless model.dependents.include?(@name)
186
194
  model.update_dependents(dependent: @name)
187
- model.update()
195
+ models_to_update << model
188
196
  end
189
197
  end
198
+ models_to_update
199
+ end
200
+
201
+ # Persist dependent changes to root triggers
202
+ def commit_roots(models)
203
+ models.each { |model| model.update() }
190
204
  end
191
205
 
192
206
  def create
193
207
  unless Store.hget(@primary_key, @name).nil?
194
208
  raise TriggerInputError.new "existing trigger found: '#{@name}'"
195
209
  end
196
- verify_triggers()
210
+ models = validate_roots()
197
211
  @updated_at = Time.now.to_nsec_from_epoch
198
212
  Store.hset(@primary_key, @name, JSON.generate(as_json, allow_nan: true))
213
+ commit_roots(models)
199
214
  notify(kind: 'created')
200
215
  end
201
216
 
202
217
  def update
203
- verify_triggers()
218
+ models = validate_roots()
204
219
  @updated_at = Time.now.to_nsec_from_epoch
205
220
  Store.hset(@primary_key, @name, JSON.generate(as_json, allow_nan: true))
221
+ commit_roots(models)
206
222
  # No notification as this is only called via trigger_controller which already notifies
207
223
  end
208
224
 
@@ -267,6 +283,7 @@ module OpenC3
267
283
  'left' => @left,
268
284
  'operator' => @operator,
269
285
  'right' => @right,
286
+ 'label' => @label,
270
287
  'updated_at' => @updated_at,
271
288
  }
272
289
  end
@@ -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
@@ -288,6 +288,7 @@ module OpenC3
288
288
  start_time = Time.now.sys
289
289
  success, value = _openc3_script_wait_implementation_comparison(target_name, packet_name, item_name, type, comparison_to_eval, timeout, polling_rate, scope: scope, token: token, &block)
290
290
  value = "'#{value}'" if value.is_a? String # Show user the check against a quoted string
291
+ value = 'nil' if value.nil? # Show user nil value as 'nil'
291
292
  time_diff = Time.now.sys - start_time
292
293
  check_str = "CHECK: #{_upcase(target_name, packet_name, item_name)}"
293
294
  if comparison_to_eval
@@ -531,7 +532,7 @@ module OpenC3
531
532
  if comparison_to_eval
532
533
  _check_eval(target_name, packet_name, item_name, comparison_to_eval, value)
533
534
  else
534
- puts "CHECK: #{_upcase(target_name, packet_name, item_name)} == #{value}"
535
+ puts "CHECK: #{_upcase(target_name, packet_name, item_name)} == #{value.nil? ? 'nil' : value.inspect}"
535
536
  end
536
537
  end
537
538
 
@@ -632,6 +633,7 @@ module OpenC3
632
633
  start_time = Time.now.sys
633
634
  success, value = _openc3_script_wait_implementation_comparison(target_name, packet_name, item_name, value_type, comparison_to_eval, timeout, polling_rate, scope: scope, token: token)
634
635
  value = "'#{value}'" if value.is_a? String # Show user the check against a quoted string
636
+ value = 'nil' if value.nil? # Show user nil value as 'nil'
635
637
  time_diff = Time.now.sys - start_time
636
638
  wait_str = "WAIT: #{_upcase(target_name, packet_name, item_name)} #{comparison_to_eval}"
637
639
  value_str = "with value == #{value} after waiting #{time_diff} seconds"
@@ -863,8 +865,20 @@ module OpenC3
863
865
  # Show user the check against a quoted string
864
866
  # Note: We have to preserve the original 'value' variable because we're going to eval against it
865
867
  value_str = value.is_a?(String) ? "'#{value}'" : value
868
+ value_str = 'nil' if value.nil? # Show user nil value as 'nil'
866
869
  with_value = "with value == #{value_str}"
867
- if eval(string)
870
+
871
+ eval_is_valid = _check_eval_validity(value, comparison_to_eval)
872
+ unless eval_is_valid
873
+ message = "Invalid comparison for types"
874
+ if $disconnect
875
+ puts "ERROR: #{message}"
876
+ else
877
+ raise CheckError, message
878
+ end
879
+ end
880
+
881
+ if eval_is_valid && eval(string)
868
882
  puts "#{check_str} success #{with_value}"
869
883
  else
870
884
  message = "#{check_str} failed #{with_value}"
@@ -883,5 +897,28 @@ module OpenC3
883
897
  raise e
884
898
  end
885
899
  end
900
+
901
+ def _check_eval_validity(value, comparison)
902
+ return true if comparison.nil? || comparison.empty?
903
+
904
+ begin
905
+ operator, operand = extract_operator_and_operand_from_comparison(comparison)
906
+ rescue RuntimeError => e
907
+ if e.message.include?("Unable to parse operand")
908
+ # If we can't parse the operand, let the eval happen anyway
909
+ # It will raise an appropriate error (like NameError for undefined constants)
910
+ return true
911
+ end
912
+ raise # Re-raise invalid operator errors
913
+ rescue JSON::ParserError
914
+ return true
915
+ end
916
+
917
+ if [">=", "<=", ">", "<"].include?(operator)
918
+ return false if value.nil? || operand.nil? || value.is_a?(Array) || operand.is_a?(Array)
919
+ end
920
+
921
+ return true
922
+ end
886
923
  end
887
924
  end
@@ -52,21 +52,37 @@ 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
- kind = kind.to_s.downcase()
57
- kinds = %w(command script reserve)
58
- unless kinds.include?(kind)
59
- raise "Unknown kind: #{kind}. Must be one of #{kinds.join(', ')}."
60
- end
61
- post_data = {}
62
- post_data['start'] = start.to_datetime.iso8601
63
- post_data['stop'] = stop.to_datetime.iso8601
64
- post_data['kind'] = kind
65
- post_data['data'] = data
64
+ post_data = _build_activity_data(kind, start, stop, data)
66
65
  response = $api_server.request('post', "/openc3-api/timeline/#{name}/activities", data: post_data, json: true, scope: scope)
67
66
  return _cal_handle_response(response, 'Failed to create timeline activity')
68
67
  end
69
68
 
69
+ # Updates an existing activity on the specified timeline.
70
+ #
71
+ # @param name [String] The name of the timeline.
72
+ # @param id [Integer] The start time / score of the activity (Unix seconds).
73
+ # @param kind [String] The kind of activity. Must be one of "COMMAND", "SCRIPT", or "RESERVE".
74
+ # @param start [DateTime] The new start time of the activity.
75
+ # @param stop [DateTime] The new stop time of the activity.
76
+ # @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 reserved for the corresponding activity kind, with "environment" also available for script activities.
77
+ # @param uuid [String] The UUID of the activity.
78
+ # @param scope [String, optional] The scope of the activity. Defaults to OPENC3_SCOPE.
79
+ def update_timeline_activity(name, id:, kind:, start:, stop:, uuid:, data: {}, scope: $openc3_scope)
80
+ post_data = _build_activity_data(kind, start, stop, data)
81
+ url = "/openc3-api/timeline/#{name}/activity/#{id}/#{uuid}"
82
+ response = $api_server.request('put', url, data: post_data, json: true, scope: scope)
83
+ return _cal_handle_response(response, 'Failed to update timeline activity')
84
+ end
85
+
70
86
  def get_timeline_activity(name, start, uuid, scope: $openc3_scope)
71
87
  response = $api_server.request('get', "/openc3-api/timeline/#{name}/activity/#{start}/#{uuid}", scope: scope)
72
88
  return _cal_handle_response(response, 'Failed to get timeline activity')
@@ -89,6 +105,20 @@ module OpenC3
89
105
  return _cal_handle_response(response, 'Failed to delete timeline activity')
90
106
  end
91
107
 
108
+ def _build_activity_data(kind, start, stop, data)
109
+ kind = kind.to_s.downcase()
110
+ kinds = %w(command script reserve)
111
+ unless kinds.include?(kind)
112
+ raise "Unknown kind: #{kind}. Must be one of #{kinds.join(', ')}."
113
+ end
114
+ {
115
+ 'start' => start.to_datetime.iso8601,
116
+ 'stop' => stop.to_datetime.iso8601,
117
+ 'kind' => kind,
118
+ 'data' => data,
119
+ }
120
+ end
121
+
92
122
  # Helper method to handle the response
93
123
  def _cal_handle_response(response, error_message)
94
124
  return nil if response.nil?