openc3 7.2.0 → 7.3.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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +108 -29
  3. data/bin/pipinstall +5 -4
  4. data/bin/pipuninstall +5 -1
  5. data/bin/uvinstall +149 -0
  6. data/data/config/interface_modifiers.yaml +15 -0
  7. data/data/config/item_modifiers.yaml +2 -1
  8. data/data/config/microservice.yaml +5 -1
  9. data/data/config/screen.yaml +2 -1
  10. data/data/config/target.yaml +38 -0
  11. data/data/config/tool.yaml +3 -1
  12. data/data/config/widgets.yaml +43 -3
  13. data/data/xtce_schemas/SpaceSystem_20180204.xsd +5918 -0
  14. data/data/xtce_schemas/xml.xsd +116 -0
  15. data/ext/openc3/ext/telemetry/telemetry.c +1 -1
  16. data/lib/openc3/accessors/binary_accessor.rb +4 -0
  17. data/lib/openc3/accessors/template_accessor.rb +2 -2
  18. data/lib/openc3/api/cmd_api.rb +5 -1
  19. data/lib/openc3/api/interface_api.rb +2 -2
  20. data/lib/openc3/api/limits_api.rb +89 -3
  21. data/lib/openc3/api/router_api.rb +3 -3
  22. data/lib/openc3/api/tlm_api.rb +1 -1
  23. data/lib/openc3/interfaces/http_client_interface.rb +1 -1
  24. data/lib/openc3/io/json_drb.rb +24 -8
  25. data/lib/openc3/io/json_rpc.rb +0 -2
  26. data/lib/openc3/logs/log_writer.rb +43 -7
  27. data/lib/openc3/logs/packet_log_writer.rb +1 -2
  28. data/lib/openc3/microservices/decom_common.rb +15 -4
  29. data/lib/openc3/microservices/decom_microservice.rb +6 -1
  30. data/lib/openc3/microservices/interface_microservice.rb +26 -12
  31. data/lib/openc3/microservices/queue_microservice.rb +4 -2
  32. data/lib/openc3/migrations/20260701000000_updated_at_to_int.rb +55 -0
  33. data/lib/openc3/migrations/20260731000000_system_health_json.rb +41 -0
  34. data/lib/openc3/models/cvt_model.rb +7 -7
  35. data/lib/openc3/models/db_sharded_model.rb +2 -2
  36. data/lib/openc3/models/gem_model.rb +1 -1
  37. data/lib/openc3/models/interface_model.rb +17 -11
  38. data/lib/openc3/models/interface_status_model.rb +2 -2
  39. data/lib/openc3/models/metric_model.rb +2 -2
  40. data/lib/openc3/models/microservice_model.rb +21 -0
  41. data/lib/openc3/models/microservice_status_model.rb +2 -2
  42. data/lib/openc3/models/model.rb +2 -6
  43. data/lib/openc3/models/plugin_model.rb +204 -49
  44. data/lib/openc3/models/python_package_model.rb +210 -12
  45. data/lib/openc3/models/reingest_job_model.rb +1 -1
  46. data/lib/openc3/models/scope_model.rb +34 -30
  47. data/lib/openc3/models/script_status_model.rb +73 -38
  48. data/lib/openc3/models/target_model.rb +193 -6
  49. data/lib/openc3/models/tool_model.rb +33 -20
  50. data/lib/openc3/operators/microservice_operator.rb +33 -4
  51. data/lib/openc3/operators/operator.rb +15 -5
  52. data/lib/openc3/packets/commands.rb +7 -7
  53. data/lib/openc3/packets/limits.rb +33 -2
  54. data/lib/openc3/packets/packet.rb +6 -4
  55. data/lib/openc3/packets/packet_config.rb +5 -1
  56. data/lib/openc3/packets/parsers/state_parser.rb +1 -1
  57. data/lib/openc3/packets/parsers/xtce_converter.rb +183 -103
  58. data/lib/openc3/packets/parsers/xtce_parser.rb +71 -23
  59. data/lib/openc3/packets/structure.rb +16 -12
  60. data/lib/openc3/packets/telemetry.rb +3 -3
  61. data/lib/openc3/script/commands.rb +1 -1
  62. data/lib/openc3/script/extract.rb +20 -25
  63. data/lib/openc3/script/limits.rb +1 -1
  64. data/lib/openc3/script/suite_runner.rb +43 -9
  65. data/lib/openc3/script/web_socket_api.rb +45 -5
  66. data/lib/openc3/system/target.rb +4 -1
  67. data/lib/openc3/tools/table_manager/table_config.rb +4 -1
  68. data/lib/openc3/top_level.rb +1 -1
  69. data/lib/openc3/topics/command_decom_topic.rb +16 -1
  70. data/lib/openc3/topics/limits_event_topic.rb +63 -4
  71. data/lib/openc3/topics/telemetry_decom_topic.rb +2 -2
  72. data/lib/openc3/utilities/aws_bucket.rb +10 -2
  73. data/lib/openc3/utilities/cli_generator.rb +5 -1
  74. data/lib/openc3/utilities/csv.rb +5 -5
  75. data/lib/openc3/utilities/env_helper.rb +13 -1
  76. data/lib/openc3/utilities/local_mode.rb +4 -1
  77. data/lib/openc3/utilities/pypi_url.rb +39 -0
  78. data/lib/openc3/utilities/reingest_job.rb +1 -1
  79. data/lib/openc3/utilities/ruby_lex_utils.rb +5 -1
  80. data/lib/openc3/utilities/running_script.rb +160 -100
  81. data/lib/openc3/utilities/script.rb +60 -13
  82. data/lib/openc3/utilities/store_autoload.rb +39 -1
  83. data/lib/openc3/utilities/store_queued.rb +5 -1
  84. data/lib/openc3/version.rb +5 -5
  85. data/lib/openc3.rb +6 -1
  86. data/templates/plugin/plugin.gemspec +1 -1
  87. data/templates/tool_angular/package.json +2 -2
  88. data/templates/tool_react/package.json +1 -1
  89. data/templates/tool_svelte/package.json +1 -1
  90. data/templates/tool_vue/package.json +3 -3
  91. data/templates/widget/package.json +2 -2
  92. metadata +7 -3
  93. data/templates/tool_vue/.nycrc +0 -3
  94. data/templates/widget/.nycrc +0 -3
@@ -25,6 +25,13 @@ module OpenC3
25
25
  class ToolModel < Model
26
26
  PRIMARY_KEY = 'openc3_tools'
27
27
 
28
+ # Tools are global to the entire COSMOS installation and are always stored
29
+ # in the DEFAULT scope. The frontend navigation always requests the tool
30
+ # list from DEFAULT, so a tool installed into any other scope would be
31
+ # inaccessible. Every scope parameter in this class is therefore ignored
32
+ # and replaced with TOOL_SCOPE.
33
+ TOOL_SCOPE = 'DEFAULT'
34
+
28
35
  attr_accessor :folder_name
29
36
  attr_accessor :icon
30
37
  attr_accessor :url
@@ -39,21 +46,24 @@ module OpenC3
39
46
 
40
47
  # NOTE: The following three class methods are used by the ModelController
41
48
  # and are reimplemented to enable various Model class methods to work
49
+ # NOTE: scope is deprecated since 7.3.0 as tools always live in the DEFAULT scope, see TOOL_SCOPE
42
50
  def self.get(name:, scope: nil)
43
- super("#{scope}__#{PRIMARY_KEY}", name: name)
51
+ super("#{TOOL_SCOPE}__#{PRIMARY_KEY}", name: name)
44
52
  end
45
53
 
54
+ # NOTE: scope is deprecated since 7.3.0 as tools always live in the DEFAULT scope, see TOOL_SCOPE
46
55
  def self.names(scope: nil)
47
56
  array = []
48
- all(scope: scope).each do |name, _tool|
57
+ all().each do |name, _tool|
49
58
  array << name
50
59
  end
51
60
  array
52
61
  end
53
62
 
63
+ # NOTE: scope is deprecated since 7.3.0 as tools always live in the DEFAULT scope, see TOOL_SCOPE
54
64
  def self.all(scope: nil)
55
65
  ordered_array = []
56
- tools = unordered_all(scope: scope)
66
+ tools = unordered_all()
57
67
  tools.each do |_name, tool|
58
68
  ordered_array << tool
59
69
  end
@@ -65,14 +75,10 @@ module OpenC3
65
75
  ordered_hash
66
76
  end
67
77
 
78
+ # Tools only exist in the DEFAULT scope. Kept for backwards compatibility
79
+ # with the ToolsController importmap endpoint.
68
80
  def self.all_scopes
69
- result = {}
70
- scopes = OpenC3::ScopeModel.all
71
- scopes.each do |key, _scope|
72
- tools = unordered_all(scope: key)
73
- result.merge!(tools)
74
- end
75
- result
81
+ unordered_all()
76
82
  end
77
83
 
78
84
  # Called by the PluginModel to allow this class to validate it's top-level keyword: "TOOL"
@@ -80,7 +86,8 @@ module OpenC3
80
86
  case keyword
81
87
  when 'TOOL'
82
88
  parser.verify_num_parameters(2, 2, "TOOL <Folder Name> <Name>")
83
- return self.new(folder_name: parameters[0], name: parameters[1], plugin: plugin, needs_dependencies: needs_dependencies, scope: scope)
89
+ # NOTE: scope is intentionally ignored, see TOOL_SCOPE
90
+ return self.new(folder_name: parameters[0], name: parameters[1], plugin: plugin, needs_dependencies: needs_dependencies, scope: TOOL_SCOPE)
84
91
  else
85
92
  raise ConfigParser::Error.new(parser, "Unknown keyword and parameters for Tool: #{keyword} #{parameters.join(" ")}")
86
93
  end
@@ -89,8 +96,10 @@ module OpenC3
89
96
 
90
97
  # The ToolsTab.vue calls the ToolsController which uses this method to reorder the tools
91
98
  # Position is index in the list starting with 0 = first
92
- def self.set_position(name:, position:, scope:)
93
- moving = from_json(get(name: name, scope: scope), scope: scope)
99
+ # NOTE: scope is deprecated since 7.3.0 as tools always live in the DEFAULT scope, see TOOL_SCOPE
100
+ def self.set_position(name:, position:, scope: nil)
101
+ tool_scope = TOOL_SCOPE
102
+ moving = from_json(get(name: name, scope: tool_scope), scope: tool_scope)
94
103
  old_pos = moving.position
95
104
  new_pos = position
96
105
  direction = :down
@@ -101,8 +110,8 @@ module OpenC3
101
110
  end
102
111
 
103
112
  # Go through all the tools and reorder
104
- all(scope: scope).each do |_tool_name, tool|
105
- tool_model = from_json(tool, scope: scope)
113
+ all(scope: tool_scope).each do |_tool_name, tool|
114
+ tool_model = from_json(tool, scope: tool_scope)
106
115
  # Update the requested model to the new position
107
116
  if tool_model.name == name
108
117
  tool_model.position = position
@@ -134,9 +143,11 @@ module OpenC3
134
143
  needs_dependencies: false,
135
144
  disable_erb: nil,
136
145
  import_map_items: nil,
137
- scope:
146
+ scope: nil # NOTE: deprecated since 7.3.0 as tools are always created in the DEFAULT scope, see TOOL_SCOPE
138
147
  )
139
- super("#{scope}__#{PRIMARY_KEY}", name: name, plugin: plugin, updated_at: updated_at, scope: scope)
148
+ # Tools are always created in the DEFAULT scope regardless of the scope
149
+ # the plugin is being installed into, see TOOL_SCOPE
150
+ super("#{TOOL_SCOPE}__#{PRIMARY_KEY}", name: name, plugin: plugin, updated_at: updated_at, scope: TOOL_SCOPE)
140
151
  @folder_name = folder_name
141
152
  @icon = icon
142
153
  @url = url
@@ -156,14 +167,15 @@ module OpenC3
156
167
  end
157
168
 
158
169
  def create(update: false, force: false, queued: false)
159
- tools = self.class.all(scope: @scope)
170
+ tools = self.class.all()
160
171
 
161
172
  # Make sure a tool with this folder_name doesn't already exist
173
+ # NOTE: Tools are global (TOOL_SCOPE) so this check is across all scopes
162
174
  unless update
163
175
  if @folder_name
164
176
  tools.each do |_tool_name, tool|
165
177
  if tool['folder_name'] == @folder_name
166
- raise "Tool with folder_name #{@folder_name} already exists at create"
178
+ raise "Tool with folder_name #{@folder_name} already exists at create. Tools are global to the entire COSMOS installation and can only be installed once."
167
179
  end
168
180
  end
169
181
  end
@@ -308,8 +320,9 @@ module OpenC3
308
320
  ##################################################
309
321
 
310
322
  # Returns the list of tools or the default OpenC3 tool set if no tools have been created
323
+ # NOTE: scope is deprecated since 7.3.0 as tools always live in the DEFAULT scope, see TOOL_SCOPE
311
324
  def self.unordered_all(scope: nil)
312
- tools = Store.hgetall("#{scope}__#{PRIMARY_KEY}")
325
+ tools = Store.hgetall("#{TOOL_SCOPE}__#{PRIMARY_KEY}")
313
326
  tools.each do |key, value|
314
327
  tools[key] = JSON.parse(value, allow_nan: true, create_additions: true)
315
328
  end
@@ -47,10 +47,39 @@ module OpenC3
47
47
  env = microservice_config["env"].dup
48
48
  if microservice_config["needs_dependencies"]
49
49
  env['GEM_HOME'] = '/gems'
50
- env['PYTHONUSERBASE'] = '/gems/python_packages'
51
- # Ensure PYTHONPATH includes both the UV venv (for base openc3 module) and user packages
52
- # This is critical for UV-based Python installations where openc3 is installed as editable
53
- env['PYTHONPATH'] = "#{ENV['PYTHONPATH']}"
50
+
51
+ # Resolve the Python virtual environment for this microservice.
52
+ # If the microservice belongs to a plugin that has a per-plugin UV venv
53
+ # (created by uvinstall during plugin install), use that isolated venv.
54
+ # Otherwise fall back to the shared PYTHONUSERBASE for legacy plugins.
55
+ plugin_name = microservice_config["plugin"]
56
+ plugin_venv_dir = nil
57
+ if plugin_name
58
+ scope = microservice_name.split("__")[0]
59
+ sanitized_name = "#{scope}__#{plugin_name}".tr('^a-zA-Z0-9_-', '_')
60
+ candidate = "/gems/plugin_venvs/#{sanitized_name}/.venv"
61
+ plugin_venv_dir = candidate if File.directory?(candidate)
62
+ end
63
+
64
+ if plugin_venv_dir
65
+ env['VIRTUAL_ENV'] = plugin_venv_dir
66
+ env['PATH'] = "#{plugin_venv_dir}/bin:#{ENV.fetch('PATH', '')}"
67
+ env['PYTHONUSERBASE'] = plugin_venv_dir
68
+ # Add the plugin venv's site-packages to PYTHONPATH so the base venv's
69
+ # Python binary can find plugin-specific packages. We keep the base binary
70
+ # because it has openc3 core installed; PYTHONPATH is always respected by
71
+ # CPython regardless of venv activation state.
72
+ site_packages = Dir.glob("#{plugin_venv_dir}/lib/python*/site-packages").first
73
+ existing_pythonpath = ENV.fetch('PYTHONPATH', '')
74
+ if site_packages
75
+ env['PYTHONPATH'] = existing_pythonpath.empty? ? site_packages : "#{site_packages}:#{existing_pythonpath}"
76
+ else
77
+ env['PYTHONPATH'] = existing_pythonpath.empty? ? nil : existing_pythonpath
78
+ end
79
+ else
80
+ env['PYTHONUSERBASE'] = '/gems/python_packages'
81
+ env['PYTHONPATH'] = ENV.fetch('PYTHONPATH', nil)
82
+ end
54
83
  else
55
84
  env['GEM_HOME'] = nil
56
85
  env['PYTHONUSERBASE'] = nil
@@ -381,11 +381,21 @@ module OpenC3
381
381
  # Monitor processes and respawn if died
382
382
  Logger.info("#{self.class} Monitoring processes every #{@cycle_time} sec...")
383
383
  loop do
384
- update()
385
- remove_old()
386
- respawn_changed()
387
- start_new()
388
- respawn_dead()
384
+ # A single cycle must never be able to take down the operator process.
385
+ # update() in particular hits Redis every cycle and a transient network
386
+ # error (the kind that also makes targets reconnect) would otherwise
387
+ # unwind run() and exit the process, which in the container looks like a
388
+ # full operator restart. Catch, log, and keep cycling so the next cycle
389
+ # can recover once Redis is reachable again.
390
+ begin
391
+ update()
392
+ remove_old()
393
+ respawn_changed()
394
+ start_new()
395
+ respawn_dead()
396
+ rescue => e
397
+ Logger.error("#{self.class} cycle error, continuing: #{e.class} #{e.message}\n#{e.backtrace.join("\n")}")
398
+ end
389
399
  break if @shutdown
390
400
 
391
401
  sleep(@cycle_time)
@@ -61,7 +61,7 @@ module OpenC3
61
61
  # target name keyed by the packet name
62
62
  def packets(target_name)
63
63
  target_packets = @config.commands[target_name.to_s.upcase]
64
- raise "Command target '#{target_name.to_s.upcase}' does not exist" unless target_packets
64
+ raise "Command target '#{target_name.to_s.upcase}' does not exist (packets lookup)" unless target_packets
65
65
 
66
66
  target_packets
67
67
  end
@@ -73,7 +73,7 @@ module OpenC3
73
73
  def packet(target_name, packet_name)
74
74
  target_packets = packets(target_name)
75
75
  packet = target_packets[packet_name.to_s.upcase]
76
- raise "Command packet '#{target_name.to_s.upcase} #{packet_name.to_s.upcase}' does not exist" unless packet
76
+ raise "Command packet '#{target_name.to_s.upcase} #{packet_name.to_s.upcase}' does not exist (packet lookup)" unless packet
77
77
 
78
78
  packet
79
79
  end
@@ -187,9 +187,9 @@ module OpenC3
187
187
 
188
188
  # Lookup the command directly - avoid redundant upcase in packet()/packets()
189
189
  target_packets = @config.commands[target_upcase]
190
- raise "Command target '#{target_upcase}' does not exist" unless target_packets
190
+ raise "Command target '#{target_upcase}' does not exist (build_cmd)" unless target_packets
191
191
  pkt = target_packets[packet_upcase]
192
- raise "Command packet '#{target_upcase} #{packet_upcase}' does not exist" unless pkt
192
+ raise "Command packet '#{target_upcase} #{packet_upcase}' does not exist (build_cmd)" unless pkt
193
193
  # Use deep_copy to avoid shared item modifications affecting the template
194
194
  # This is critical for variable_bit_size items where handle_write_variable_bit_size
195
195
  # modifies item.bit_offset and item.array_size during writes
@@ -325,10 +325,10 @@ module OpenC3
325
325
  unless item.states.values.include?(value)
326
326
  if command.raw
327
327
  # Raw commands report missing value maps
328
- raise "Command parameter '#{command.target_name} #{command.packet_name} #{item_upcase}' = #{value.to_s.upcase} not one of #{item.states.values.join(', ')}"
328
+ raise RangeError, "Command parameter '#{command.target_name} #{command.packet_name} #{item_upcase}' = #{value.to_s.upcase} not one of #{item.states.values.join(', ')}"
329
329
  else
330
330
  # Normal commands report missing state maps
331
- raise "Command parameter '#{command.target_name} #{command.packet_name} #{item_upcase}' = #{value.to_s.upcase} not one of #{item.states.keys.join(', ')}"
331
+ raise RangeError, "Command parameter '#{command.target_name} #{command.packet_name} #{item_upcase}' = #{value.to_s.upcase} not one of #{item.states.keys.join(', ')}"
332
332
  end
333
333
  end
334
334
  end
@@ -340,7 +340,7 @@ module OpenC3
340
340
  # Perform Range Check on command parameter
341
341
  if not range.include?(range_check_value)
342
342
  range_check_value = "'#{range_check_value}'" if String === range_check_value
343
- raise "Command parameter '#{command.target_name} #{command.packet_name} #{item_upcase}' = #{range_check_value} not in valid range of #{range.first} to #{range.last}"
343
+ raise RangeError, "Command parameter '#{command.target_name} #{command.packet_name} #{item_upcase}' = #{range_check_value} not in valid range of #{range.first} to #{range.last}"
344
344
  end
345
345
  end
346
346
  end
@@ -153,16 +153,47 @@ module OpenC3
153
153
  return [limits_set, limits.persistence_setting, limits.enabled, limits_for_set[0], limits_for_set[1], limits_for_set[2], limits_for_set[3], limits_for_set[4], limits_for_set[5]]
154
154
  end
155
155
 
156
+ # Set the color for a telemetry item state
157
+ #
158
+ # @param target_name [String] Target Name
159
+ # @param packet_name [String] Packet Name
160
+ # @param item_name [String] Item Name
161
+ # @param state_name [String] State Name (e.g. 'CONNECTED')
162
+ # @param color [String or Symbol, nil] New color: GREEN, YELLOW, or RED.
163
+ # Pass nil to clear (remove) the state color.
164
+ # @return [Symbol, nil] The color that was set, or nil if the color was cleared
165
+ def set_state_color(target_name, packet_name, item_name, state_name, color)
166
+ packet = _get_packet(target_name, packet_name)
167
+ item = packet.get_item(item_name)
168
+ raise "Item '#{target_name} #{packet_name} #{item_name}' does not have any states" unless item.states
169
+ state_name = state_name.to_s.upcase
170
+ raise "State '#{state_name}' does not exist for item '#{target_name} #{packet_name} #{item_name}'" unless item.states.key?(state_name)
171
+ if color.nil?
172
+ # Clear the state color while keeping state_colors as a Hash so limits
173
+ # checking (which indexes state_colors by value) continues to work.
174
+ item.state_colors.delete(state_name) if item.state_colors
175
+ packet.update_limits_items_cache(item)
176
+ return nil
177
+ end
178
+ color = color.to_s.upcase.intern
179
+ raise "Invalid state color '#{color}'. Must be one of #{PacketItem::STATE_COLORS.join(', ')}." unless PacketItem::STATE_COLORS.include?(color)
180
+ item.state_colors ||= {}
181
+ item.state_colors[state_name] = color
182
+ item.limits.enabled = true
183
+ packet.update_limits_items_cache(item)
184
+ return color
185
+ end
186
+
156
187
  protected
157
188
 
158
189
  def _get_packet(target_name, packet_name)
159
190
  raise "LATEST packet not valid" if packet_name.upcase == LATEST_PACKET_NAME
160
191
 
161
192
  packets = @config.telemetry[target_name.to_s.upcase]
162
- raise "Telemetry target '#{target_name.to_s.upcase}' does not exist" unless packets
193
+ raise "Telemetry target '#{target_name.to_s.upcase}' does not exist (limits lookup)" unless packets
163
194
 
164
195
  packet = packets[packet_name.to_s.upcase]
165
- raise "Telemetry packet '#{target_name.to_s.upcase} #{packet_name.to_s.upcase}' does not exist" unless packet
196
+ raise "Telemetry packet '#{target_name.to_s.upcase} #{packet_name.to_s.upcase}' does not exist (limits lookup)" unless packet
166
197
 
167
198
  return packet
168
199
  end
@@ -616,7 +616,7 @@ module OpenC3
616
616
  def get_item(name)
617
617
  return super(name)
618
618
  rescue ArgumentError
619
- raise "Packet item '#{@target_name} #{@packet_name} #{name.upcase}' does not exist"
619
+ raise "Item '#{@target_name} #{@packet_name} #{name.upcase}' does not exist (Packet)"
620
620
  end
621
621
 
622
622
  # Read an item in the packet
@@ -1285,7 +1285,7 @@ module OpenC3
1285
1285
  config
1286
1286
  end
1287
1287
 
1288
- def decom
1288
+ def decom(include_limits_states: true)
1289
1289
  # Read all the RAW at once because this could be optimized by the accessor
1290
1290
  json_hash = read_items(@sorted_items)
1291
1291
 
@@ -1301,7 +1301,9 @@ module OpenC3
1301
1301
  if item.format_string or item.units
1302
1302
  json_hash["#{item.name}__F"] = read_item(item, :FORMATTED, @buffer, given_raw)
1303
1303
  end
1304
- limits_state = item.limits.state
1304
+ if include_limits_states
1305
+ limits_state = item.limits.state
1306
+ end
1305
1307
  if limits_state
1306
1308
  json_hash["#{item.name}__L"] = limits_state
1307
1309
  end
@@ -1551,4 +1553,4 @@ module OpenC3
1551
1553
  item
1552
1554
  end
1553
1555
  end
1554
- end
1556
+ end
@@ -161,7 +161,11 @@ module OpenC3
161
161
  process_target_name = process_target_name.upcase
162
162
  parser = ConfigParser.new("https://docs.openc3.com/docs")
163
163
  parser.instance_variable_set(:@target_name, process_target_name)
164
- parser.parse_file(filename) do |keyword, params|
164
+ # run_erb is false: ERB is only rendered once, at plugin install time
165
+ # (TargetModel#deploy), before files are written to the targets bucket. The
166
+ # targets_modified overlay (e.g. dynamically created packets) is never ERB
167
+ # rendered, so it is treated as data here rather than executed as code.
168
+ parser.parse_file(filename, false, true, false) do |keyword, params|
165
169
  if @building_generic_conversion
166
170
  case keyword
167
171
  # Complete a generic conversion
@@ -124,7 +124,7 @@ module OpenC3
124
124
  def get_state_colors(item)
125
125
  color = @parser.parameters[2].upcase.to_sym
126
126
  unless PacketItem::STATE_COLORS.include? color
127
- raise @parser.error("Invalid state color #{color}. Must be one of #{PacketItem::STATE_COLORS.join(' ')}.", @usage)
127
+ raise @parser.error("Invalid state color '#{color}'. Must be one of #{PacketItem::STATE_COLORS.join(', ')}.", @usage)
128
128
  end
129
129
 
130
130
  item.limits.enabled = true