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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 90e17f6bf28158ea786646171220e3c23aca07cce30559b3e64a29f83dabb50d
4
- data.tar.gz: 9a13e73273ba67e7d6003a9578a34a8b1c0abadea3b8d4d369f1fd3803a4f0ec
3
+ metadata.gz: 787269f06fae8562e23d790d3c4887e2f913841dc818570fdafa06b29fd1b8e6
4
+ data.tar.gz: c6630dbc9c61bf71f96015db09ca9246ef80247f4f1b5de429c97b537551c4e4
5
5
  SHA512:
6
- metadata.gz: eec5ccdb945747570717e5f60f9a89158d500d994f1fa5e6ebc81ed8bb263229950f10e1f277e0c68bf5d9fb531f3470354d45fda2164f7608c9ad22413b2022
7
- data.tar.gz: a32f7675eacf6b41b345a4b5ae554292db011499d12a9e5e65173c514e5fac7e66da3bacea7ad5cf9a3738fcb96e3a82009997993fc70722d4a9ba4edd236261
6
+ metadata.gz: b72db91c6fd2febea1855d452a30e496b33bc5c19010ebc674ef044fea01d8cff1c921abcee824c39f32434e66fa9b4ed040bc1f164e9ea7cec99be431aec03a
7
+ data.tar.gz: 0ffeaa70a064e4d23cd49326e87b85e4eafefd0d4fa61a3db60fc503a8e48593c349588f4f28e374f4c7ba87408a66c072e822101f689bfc58cb3bcf567af966
data/bin/openc3cli CHANGED
@@ -67,6 +67,7 @@ def print_usage
67
67
  puts " cli load /PATH/FILENAME.gem SCOPE plugin_hash.json # Loads a COSMOS plugin gem file"
68
68
  puts " OPTIONS: --variables lets you pass a path to a JSON file containing your plugin's variables"
69
69
  puts " cli list <SCOPE> # Lists installed plugins, SCOPE is DEFAULT if not given"
70
+ puts " cli unload PLUGIN_NAME [SCOPE] # Unload an installed COSMOS plugin"
70
71
  puts " cli generate TYPE OPTIONS # Generate various COSMOS entities"
71
72
  puts " OPTIONS: --ruby or --python is required to specify the language in the generated code unless OPENC3_LANGUAGE is set"
72
73
  puts " cli bridge CONFIG_FILENAME # Run COSMOS host bridge"
@@ -77,6 +78,7 @@ def print_usage
77
78
  puts " cli xtce_converter # Convert to and from the XTCE format. Run with --help for more info."
78
79
  puts " cli cstol_converter # Converts CSTOL files (.prc) to COSMOS. Run with --help for more info."
79
80
  puts " cli setpassword # Set the initial password from OPENC3_API_PASSWORD env var"
81
+ puts " cli migratetouv PLUGIN_NAME SCOPE # Migrate plugin to per-plugin UV virtual environment"
80
82
  puts ""
81
83
  end
82
84
 
@@ -102,6 +104,42 @@ def check_environment
102
104
  end
103
105
  end
104
106
 
107
+ # Report how an existing .xtce file fares against the XTCE 1.2 schema without failing
108
+ def xtce_report_validation(filename)
109
+ namespace = OpenC3::XtceConverter.xtce_namespace(filename)
110
+ if namespace != OpenC3::XtceConverter::XTCE_1_2_NAMESPACE
111
+ puts "#{File.basename(filename)} declares #{namespace ? namespace : 'no XTCE namespace'}, skipping XTCE 1.2 schema validation"
112
+ return
113
+ end
114
+ errors = OpenC3::XtceConverter.schema_errors(filename)
115
+ if errors.empty?
116
+ puts "#{File.basename(filename)} is valid XTCE 1.2"
117
+ else
118
+ $stderr.puts "WARNING: #{filename} is not valid XTCE 1.2:"
119
+ errors.each { |error| $stderr.puts " #{error}" }
120
+ end
121
+ end
122
+
123
+ # Validate every .xtce file written under output_dir. Returns false if any is invalid.
124
+ def xtce_validate_exported(output_dir)
125
+ files = Dir.glob(File.join(output_dir, "**", "*.xtce")).sort
126
+ if files.empty?
127
+ $stderr.puts "ERROR: no .xtce files were written to #{output_dir}"
128
+ return false
129
+ end
130
+ valid = true
131
+ files.each do |filename|
132
+ errors = OpenC3::XtceConverter.schema_errors(filename)
133
+ next if errors.empty?
134
+
135
+ valid = false
136
+ $stderr.puts "ERROR: #{filename} is not valid XTCE 1.2:"
137
+ errors.each { |error| $stderr.puts " #{error}" }
138
+ end
139
+ puts "Validated #{files.length} .xtce file(s) against the OMG XTCE 1.2 schema" if valid
140
+ valid
141
+ end
142
+
105
143
  def xtce_converter(args)
106
144
  options = {}
107
145
  option_parser = OptionParser.new do |opts|
@@ -122,14 +160,17 @@ def xtce_converter(args)
122
160
  opts.on("-p", "--plugin PLUGIN", "Export .xtce file(s) from the plugin") do |arg|
123
161
  options[:plugin] = arg
124
162
  end
125
- opts.on("-v", "--variables", "Optional variables file to pass to the plugin") do |arg|
163
+ opts.on("-v VARIABLES", "--variables VARIABLES", "Optional variables file to pass to the plugin") do |arg|
126
164
  options[:variables] = arg
127
165
  end
128
166
  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|
129
167
  options[:root_target_name] = arg
130
168
  end
131
- 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|
132
- options[:time_association_name] = "PACKET_TIME"
169
+ opts.on("-t TIME_ASSOCIATION_NAME", "--time_association_name TIME_ASSOCIATION_NAME", "Optional telemetry item name to export as each packet's time, emitted as a TimeAssociation. If not specified, no TimeAssociation is written") do |arg|
170
+ options[:time_association_name] = arg
171
+ end
172
+ opts.on("--[no-]validate", "Validate .xtce files against the OMG XTCE 1.2 schema. On by default when exporting, where an invalid file fails the export. Off by default when importing, where it only reports since COSMOS also imports XTCE 1.0 / 1.1") do |arg|
173
+ options[:validate] = arg
133
174
  end
134
175
  end
135
176
 
@@ -149,6 +190,9 @@ def xtce_converter(args)
149
190
  OpenC3::Logger.level = OpenC3::Logger::DEBUG
150
191
 
151
192
  if options[:import] && options[:output]
193
+ # Reporting only. COSMOS imports XTCE 1.0 / 1.1 too, and those report errors against
194
+ # the 1.2 schema while importing perfectly well, so this never blocks an import.
195
+ xtce_report_validation(options[:import]) if options[:validate]
152
196
  packet_config = OpenC3::PacketConfig.new
153
197
  puts "Processing #{options[:import]}..."
154
198
  packet_config.process_file(options[:import], nil)
@@ -166,7 +210,14 @@ def xtce_converter(args)
166
210
  OpenC3::PluginModel.install_phase2(plugin_hash, scope: 'DEFAULT', validate_only: true,
167
211
  gem_file_path: options[:plugin])
168
212
  OpenC3::XtceConverter.combine_output_xtce(options[:output], options[:root_target_name])
169
- result = 0 # bash and Windows consider 0 success
213
+ # Validate what we just wrote unless told not to. An exporter bug that only shows
214
+ # up on a particular config otherwise produces a file that looks fine here and is
215
+ # rejected by whatever ground system it was written for.
216
+ if options[:validate] == false
217
+ result = 0
218
+ else
219
+ result = xtce_validate_exported(options[:output]) ? 0 : ERROR_CODE
220
+ end # bash and Windows consider 0 success
170
221
  rescue => e
171
222
  puts "Error: #{e.message}"
172
223
  puts e.backtrace
@@ -226,7 +277,7 @@ ensure
226
277
  exit(result)
227
278
  end
228
279
 
229
- def update_plugin(plugin_file_path, plugin_name, variables: nil, plugin_txt_lines: nil, scope:, existing_plugin_name:, force: false)
280
+ def update_plugin(plugin_file_path, plugin_name, variables: nil, plugin_txt_lines: nil, scope:, existing_plugin_name:, force: false, username: nil, version_history_files: nil)
230
281
  new_gem = File.basename(plugin_file_path)
231
282
  old_gem = existing_plugin_name.split("__")[0]
232
283
  puts "Updating existing plugin: #{existing_plugin_name} with #{File.basename(plugin_file_path)}"
@@ -247,6 +298,10 @@ def update_plugin(plugin_file_path, plugin_name, variables: nil, plugin_txt_line
247
298
  plugin_model.destroy
248
299
 
249
300
  plugin_hash = OpenC3::PluginModel.install_phase1(plugin_file_path, existing_variables: variables, existing_plugin_txt_lines: plugin_txt_lines, process_existing: true, scope: scope)
301
+ # Carry Version History upgrade hints into the regenerated hash so
302
+ # install_phase2 can version modified files taken from the plugin.
303
+ plugin_hash['username'] = username if username
304
+ plugin_hash['version_history_files'] = version_history_files if version_history_files
250
305
  puts "Updating plugin: #{plugin_file_path}\n#{plugin_hash}"
251
306
  plugin_hash = OpenC3::PluginModel.install_phase2(plugin_hash, scope: scope)
252
307
  OpenC3::LocalMode.update_local_plugin(plugin_file_path, plugin_hash, old_plugin_name: plugin_name, scope: scope)
@@ -410,7 +465,8 @@ def load_plugin(plugin_file_path, scope:, plugin_hash_file: nil, force: false, v
410
465
  if existing_plugin_hash
411
466
  # Upgrade or Edit
412
467
  update_plugin(plugin_file_path, plugin_hash['name'], variables: plugin_hash['variables'], scope: scope,
413
- plugin_txt_lines: plugin_hash['plugin_txt_lines'], existing_plugin_name: existing_plugin_hash['name'], force: force)
468
+ plugin_txt_lines: plugin_hash['plugin_txt_lines'], existing_plugin_name: existing_plugin_hash['name'], force: force,
469
+ username: plugin_hash['username'], version_history_files: plugin_hash['version_history_files'])
414
470
  else
415
471
  # New Install
416
472
  puts "Loading new plugin: #{plugin_file_path}\n#{plugin_hash}"
@@ -447,6 +503,10 @@ def unload_plugin(plugin_name, scope:)
447
503
  # Remove the backing gem now that no PluginModel references it,
448
504
  # so it disappears from the admin Packages tab.
449
505
  OpenC3::PluginModel.cleanup_gem(plugin_name, scope: scope)
506
+ # Drop this plugin's Version History repo. Uninstall-only: the upgrade
507
+ # path (update_plugin) reuses the repo and never reaches here, so history
508
+ # survives version bumps. No-op in Core / when versioning is disabled.
509
+ OpenC3::TargetModel.destroy_script_versions(plugin_name, scope: scope)
450
510
  OpenC3::Logger.info("PluginModel destroyed: #{plugin_name}", scope: scope)
451
511
  rescue => e
452
512
  abort("Error uninstalling plugin: #{scope}: #{plugin_name}: #{e.formatted}")
@@ -671,30 +731,18 @@ def cli_script_list(args, options)
671
731
  end
672
732
 
673
733
  def parse_suite_runner_options(options)
674
- suite_runner = nil
675
734
  # suite must be given to enable Suite Runner execution
676
- if options[:suite]
677
- suite_runner = {}
678
- suite_runner['suite'] = options[:suite]
679
- if options[:group]
680
- suite_runner['group'] = options[:group]
681
- # script requires group to be set
682
- if options[:script]
683
- suite_runner['script'] = options[:script]
684
- end
685
- end
686
- if options[:method]
687
- suite_runner['method'] = options[:method]
688
- else
689
- suite_runner['method'] = 'start'
690
- end
691
- if options[:options]
692
- suite_runner['options'] = options[:options].split(',')
693
- else
694
- suite_runner['options'] = ["continueAfterError"]
695
- end
696
- end
697
- return suite_runner
735
+ return nil unless options[:suite]
736
+ require 'openc3/script/suite_runner'
737
+ # Build the canonical hash via the shared helper so the CLI applies the same
738
+ # shape, defaults, and validation (e.g. script requires group) as execution.
739
+ return OpenC3::SuiteRunner.build_options(
740
+ suite: options[:suite],
741
+ group: options[:group],
742
+ script: options[:script],
743
+ method: options[:method],
744
+ options: options[:options]&.split(','),
745
+ )
698
746
  end
699
747
 
700
748
  def cli_script_run(args, options)
@@ -1475,6 +1523,37 @@ if not ARGV[0].nil? # argument(s) given
1475
1523
  when 'migratepassword'
1476
1524
  migrate_password_hash()
1477
1525
 
1526
+ when 'migratetouv'
1527
+ if ARGV[1].nil? || ARGV[1] == '--help' || ARGV[1] == '-h'
1528
+ puts "Usage: cli migratetouv PLUGIN_NAME [SCOPE]"
1529
+ puts ""
1530
+ puts "Migrate a plugin from the shared Python venv to a per-plugin UV virtual environment"
1531
+ puts ""
1532
+ puts "Arguments:"
1533
+ puts " PLUGIN_NAME Name of the plugin to migrate (required)"
1534
+ puts " SCOPE Scope of the plugin (optional, default: DEFAULT)"
1535
+ puts ""
1536
+ puts "Options:"
1537
+ puts " -h, --help Show this help message"
1538
+ exit(ARGV[1].nil? ? 1 : 0)
1539
+ end
1540
+ scope = ARGV[2] || 'DEFAULT'
1541
+ check_environment()
1542
+ begin
1543
+ plugin_model = OpenC3::PluginModel.get_model(name: ARGV[1], scope: scope)
1544
+ if plugin_model.nil?
1545
+ abort("Plugin '#{ARGV[1]}' not found in scope '#{scope}'")
1546
+ end
1547
+ if plugin_model.migrate_to_uv!(scope: scope)
1548
+ puts "Successfully migrated plugin '#{ARGV[1]}' to per-plugin UV venv"
1549
+ else
1550
+ puts "Migration failed for plugin '#{ARGV[1]}'. Check logs for details."
1551
+ exit 1
1552
+ end
1553
+ rescue => e
1554
+ abort("Error migrating plugin: #{e.formatted}")
1555
+ end
1556
+
1478
1557
  else # Unknown task
1479
1558
  print_usage()
1480
1559
  abort("Unknown task: #{ARGV[0]}")
data/bin/pipinstall CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/bin/sh
2
- uv venv "$PYTHONUSERBASE" --allow-existing
2
+ VENV="${PIPINSTALL_VENV:-$PYTHONUSERBASE}"
3
+ uv venv "$VENV" --allow-existing
3
4
  echo "uv pip install $@"
4
- uv pip install --python "$PYTHONUSERBASE" "$@"
5
+ uv pip install --python "$VENV" "$@"
5
6
  if [ $? -eq 0 ]; then
6
7
  echo "Command succeeded"
7
8
  exit 0
@@ -27,7 +28,7 @@ if [ -d "$LAST_ARG" ] && [ -f "$LAST_ARG/pyproject.toml" ]; then
27
28
  TMPFILE=$(mktemp)
28
29
  uv pip compile ${OPTS} "${LAST_ARG}/pyproject.toml" > "$TMPFILE"
29
30
  if [ $? -eq 0 ] && [ -s "$TMPFILE" ]; then
30
- uv pip install --python "$PYTHONUSERBASE" ${OPTS} -r "$TMPFILE"
31
+ uv pip install --python "$VENV" ${OPTS} -r "$TMPFILE"
31
32
  if [ $? -eq 0 ]; then
32
33
  echo "Dependencies installed successfully"
33
34
  rm -f "$TMPFILE"
@@ -38,7 +39,7 @@ if [ -d "$LAST_ARG" ] && [ -f "$LAST_ARG/pyproject.toml" ]; then
38
39
  fi
39
40
 
40
41
  echo "Warning: Install failed - retrying with --no-index"
41
- uv pip install --python "$PYTHONUSERBASE" --no-index "$@"
42
+ uv pip install --python "$VENV" --no-index "$@"
42
43
  if [ $? -ne 0 ]; then
43
44
  echo "ERROR: uv pip install failed"
44
45
  exit 1
data/bin/pipuninstall CHANGED
@@ -1,6 +1,10 @@
1
1
  #!/bin/sh
2
+ # Uninstall a Python package from a venv. When PIPINSTALL_VENV is set (by
3
+ # PythonPackageModel.destroy for per-plugin venvs), uninstall from that venv.
4
+ # Otherwise fall back to the shared PYTHONUSERBASE.
5
+ VENV="${PIPINSTALL_VENV:-$PYTHONUSERBASE}"
2
6
  echo "uv pip uninstall $@"
3
- uv pip uninstall --python "$PYTHONUSERBASE" "$@"
7
+ uv pip uninstall --python "$VENV" "$@"
4
8
  if [ $? -eq 0 ]; then
5
9
  echo "Command succeeded"
6
10
  else
data/bin/uvinstall ADDED
@@ -0,0 +1,149 @@
1
+ #!/bin/sh
2
+ # Per-plugin UV virtual environment installer
3
+ # Usage: uvinstall <plugin_venv_name> <gem_path> [extra uv args...]
4
+ #
5
+ # Creates an isolated venv at /gems/plugin_venvs/<plugin_venv_name>/.venv
6
+ # and installs Python dependencies from the plugin's gem_path.
7
+ #
8
+ # Two installation paths:
9
+ # 1. If gem_path contains uv.lock: uses `uv sync --frozen` (reproducible)
10
+ # 2. Otherwise: falls back to `uv pip install` for requirements.txt / pyproject.toml
11
+ #
12
+ # Network safety: uv's own HTTP controls (connect timeout, read timeout,
13
+ # bounded retries) make a slow or unreachable index fail fast instead of
14
+ # hanging the plugin install (which stalls the whole deploy - the init hook
15
+ # never completes and helm's --wait times out). These are idle/connect
16
+ # timeouts, NOT a total-transfer cap, so a large but healthy download (e.g.
17
+ # torch over a slow link) is never killed mid-stream as long as bytes keep
18
+ # arriving. Both paths try OFFLINE first so a plugin whose wheels are already
19
+ # in the seeded UV cache installs deterministically without ever touching the
20
+ # network.
21
+
22
+ PLUGIN_VENV_NAME="$1"
23
+ GEM_PATH="$2"
24
+ shift 2
25
+ UV_ARGS="$@"
26
+
27
+ if [ -z "$PLUGIN_VENV_NAME" ] || [ -z "$GEM_PATH" ]; then
28
+ echo "Usage: uvinstall <plugin_venv_name> <gem_path> [extra uv args...]"
29
+ exit 1
30
+ fi
31
+
32
+ # Bound uv's network behavior with uv's own HTTP knobs. Unlike a blunt
33
+ # wall-clock cap, these fail fast on a dead/unreachable index or a stalled
34
+ # connection WITHOUT killing a large but progressing download. All overridable.
35
+ # UV_HTTP_CONNECT_TIMEOUT - give up connecting to an unreachable index (s)
36
+ # UV_HTTP_TIMEOUT - give up when a connection stalls mid-read (s)
37
+ # UV_HTTP_RETRIES - bounded retries for transient failures
38
+ # The values below match uv's own defaults; they are set explicitly so plugin
39
+ # installs keep this behavior even if a future uv release changes its defaults,
40
+ # and so an operator has a documented place to raise them for a slow index.
41
+ export UV_HTTP_CONNECT_TIMEOUT="${UV_HTTP_CONNECT_TIMEOUT:-10}"
42
+ export UV_HTTP_TIMEOUT="${UV_HTTP_TIMEOUT:-30}"
43
+ export UV_HTTP_RETRIES="${UV_HTTP_RETRIES:-3}"
44
+
45
+ # Run a uv command offline first (cache-only: instant, cannot hang, works in
46
+ # air-gapped clusters) and only retry online on a cache miss. Both paths below
47
+ # use this so a plugin whose wheels are already in the seeded UV cache never
48
+ # touches the network.
49
+ uv_offline_then_online() {
50
+ if uv "$@" --offline; then
51
+ return 0
52
+ fi
53
+ # Expected for any plugin whose wheels aren't in the seeded cache (a new
54
+ # third-party plugin, or a pin that differs from the one baked in), so this
55
+ # is informational, not an error - the online attempt below is the real one.
56
+ echo "Offline uv cache miss; retrying online"
57
+ uv "$@"
58
+ }
59
+
60
+ VENV_BASE="/gems/plugin_venvs/${PLUGIN_VENV_NAME}"
61
+ VENV_DIR="${VENV_BASE}/.venv"
62
+
63
+ echo "Creating per-plugin venv for '${PLUGIN_VENV_NAME}' at ${VENV_DIR}"
64
+ mkdir -p "${VENV_BASE}"
65
+
66
+ # Path 1: uv.lock present - use `uv sync --frozen` for reproducible installs
67
+ if [ -f "${GEM_PATH}/uv.lock" ] && [ -f "${GEM_PATH}/pyproject.toml" ]; then
68
+ echo "Found uv.lock - using uv sync --frozen for reproducible install"
69
+
70
+ # Copy pyproject.toml and uv.lock into the venv base directory
71
+ cp "${GEM_PATH}/pyproject.toml" "${VENV_BASE}/pyproject.toml"
72
+ cp "${GEM_PATH}/uv.lock" "${VENV_BASE}/uv.lock"
73
+
74
+ # Create venv and sync dependencies
75
+ cd "${VENV_BASE}"
76
+ uv venv "${VENV_DIR}" --allow-existing
77
+
78
+ # Offline sync first, online only on a cache miss (bounded by UV_HTTP_*)
79
+ uv_offline_then_online sync --frozen --no-dev --no-install-project ${UV_ARGS}
80
+ RESULT=$?
81
+
82
+ if [ $RESULT -eq 0 ]; then
83
+ echo "uv sync --frozen succeeded"
84
+ touch "${VENV_BASE}/.uv_managed"
85
+ exit 0
86
+ fi
87
+
88
+ echo "Warning: uv sync --frozen failed (exit code ${RESULT}), falling back to uv pip install"
89
+ fi
90
+
91
+ # Path 2: Fallback - use uv pip install for requirements.txt or pyproject.toml
92
+ # Every install here is also offline-first (same reasoning as path 1), so an
93
+ # unlocked plugin whose wheels happen to be cached installs without network too.
94
+ uv venv "${VENV_DIR}" --allow-existing
95
+
96
+ if [ -f "${GEM_PATH}/requirements.txt" ]; then
97
+ echo "Installing from requirements.txt via uv pip install"
98
+ uv_offline_then_online pip install --python "${VENV_DIR}" ${UV_ARGS} -r "${GEM_PATH}/requirements.txt"
99
+ RESULT=$?
100
+ elif [ -f "${GEM_PATH}/pyproject.toml" ]; then
101
+ echo "Installing from pyproject.toml via uv pip install"
102
+ uv_offline_then_online pip install --python "${VENV_DIR}" ${UV_ARGS} "${GEM_PATH}"
103
+ RESULT=$?
104
+
105
+ # `uv pip install <path>` builds the plugin as a package before installing
106
+ # its dependencies, which fails for a pyproject.toml that only declares
107
+ # dependencies and isn't a buildable distribution (no [build-system], a
108
+ # missing/empty source layout, or a build backend that can't run offline).
109
+ # Fall back to installing the declared dependencies without building. The
110
+ # compile step only resolves declared deps into a pinned requirements file;
111
+ # the install that follows it is the one that fetches, so it gets the same
112
+ # offline-then-online treatment. Each compile attempt re-truncates TMPFILE
113
+ # so a partial offline write can never be mixed into the online result.
114
+ if [ $RESULT -ne 0 ]; then
115
+ echo "Warning: Failed to build Python package, attempting to install declared dependencies from pyproject.toml"
116
+ TMPFILE=$(mktemp)
117
+ uv pip compile --offline ${UV_ARGS} "${GEM_PATH}/pyproject.toml" > "$TMPFILE"
118
+ COMPILED=$?
119
+ if [ $COMPILED -ne 0 ] || [ ! -s "$TMPFILE" ]; then
120
+ echo "Offline uv pip compile missed the cache; retrying online"
121
+ uv pip compile ${UV_ARGS} "${GEM_PATH}/pyproject.toml" > "$TMPFILE"
122
+ COMPILED=$?
123
+ fi
124
+ # Both the exit status AND a non-empty file are required: uv writes its
125
+ # "autogenerated by uv" header before resolution runs, so a compile that
126
+ # fails partway still leaves a non-empty file. Installing from that would
127
+ # produce a venv missing dependencies and report success.
128
+ if [ $COMPILED -eq 0 ] && [ -s "$TMPFILE" ]; then
129
+ uv_offline_then_online pip install --python "${VENV_DIR}" ${UV_ARGS} -r "$TMPFILE"
130
+ RESULT=$?
131
+ fi
132
+ rm -f "$TMPFILE"
133
+ fi
134
+ else
135
+ echo "ERROR: No requirements.txt, pyproject.toml, or uv.lock found in ${GEM_PATH}"
136
+ exit 1
137
+ fi
138
+
139
+ if [ $RESULT -eq 0 ]; then
140
+ echo "Python packages installed successfully"
141
+ touch "${VENV_BASE}/.uv_managed"
142
+ exit 0
143
+ fi
144
+
145
+ # No --no-index last resort here anymore: every attempt above already ran
146
+ # cache-only first via uv_offline_then_online, so a cache-only retry at this
147
+ # point would repeat an attempt that has already failed.
148
+ echo "ERROR: uvinstall failed for plugin ${PLUGIN_VENV_NAME}"
149
+ exit 1
@@ -1,11 +1,16 @@
1
1
  ---
2
2
  MAP_TARGET:
3
3
  summary: Maps a target name to an interface
4
+ description: See [Mapping Targets to Interfaces](/docs/configuration/interfaces#mapping-targets-to-interfaces) for more information.
4
5
  parameters:
5
6
  - name: Target Name
6
7
  required: true
7
8
  description: Target name to map to this interface
8
9
  values: .+
10
+ - name: Enabled State
11
+ required: false
12
+ description: Initial enabled state of the target on this interface. Defaults to ENABLED.
13
+ values: ["ENABLED", "DISABLED"]
9
14
  ruby_example: |
10
15
  INTERFACE DATA_INT tcpip_client_interface.rb host.docker.internal 8080 8081 10.0 nil BURST
11
16
  MAP_TARGET DATA
@@ -14,12 +19,17 @@ MAP_TARGET:
14
19
  MAP_TARGET DATA
15
20
  MAP_CMD_TARGET:
16
21
  summary: Maps a target name to an interface for commands only
22
+ description: See [Mapping Targets to Interfaces](/docs/configuration/interfaces#mapping-targets-to-interfaces) for more information.
17
23
  since: 5.2.0
18
24
  parameters:
19
25
  - name: Target Name
20
26
  required: true
21
27
  description: Command target name to map to this interface
22
28
  values: .+
29
+ - name: Enabled State
30
+ required: false
31
+ description: Initial enabled state of the command target on this interface. Defaults to ENABLED.
32
+ values: ["ENABLED", "DISABLED"]
23
33
  ruby_example: |
24
34
  INTERFACE CMD_INT tcpip_client_interface.rb host.docker.internal 8080 8081 10.0 nil BURST
25
35
  MAP_CMD_TARGET DATA # Only DATA commands go on the CMD_INT interface
@@ -28,12 +38,17 @@ MAP_CMD_TARGET:
28
38
  MAP_CMD_TARGET DATA # Only DATA commands go on the CMD_INT interface
29
39
  MAP_TLM_TARGET:
30
40
  summary: Maps a target name to an interface for telemetry only
41
+ description: See [Mapping Targets to Interfaces](/docs/configuration/interfaces#mapping-targets-to-interfaces) for more information.
31
42
  since: 5.2.0
32
43
  parameters:
33
44
  - name: Target Name
34
45
  required: true
35
46
  description: Telemetry target name to map to this interface
36
47
  values: .+
48
+ - name: Enabled State
49
+ required: false
50
+ description: Initial enabled state of the telemetry target on this interface. Defaults to ENABLED.
51
+ values: ["ENABLED", "DISABLED"]
37
52
  ruby_example: |
38
53
  INTERFACE TLM_INT tcpip_client_interface.rb host.docker.internal 8080 8081 10.0 nil BURST
39
54
  MAP_TLM_TARGET DATA # Only DATA telemetry received on TLM_INT interface
@@ -136,7 +136,8 @@ LIMITS:
136
136
  - name: Persistence
137
137
  required: true
138
138
  description: Number of consecutive times the telemetry item must be within
139
- a different limits range before changing limits state.
139
+ a different limits range before changing limits state. Note if you have
140
+ multiple LIMITS items they should all have the same persistence value.
140
141
  values: \d+
141
142
  - name: Initial State
142
143
  required: true
@@ -117,7 +117,11 @@ MICROSERVICE:
117
117
  values: .+
118
118
  SECRET:
119
119
  summary: Define a secret needed by this microservice
120
- description: Defines a secret for this microservice. For more information see [Admin Secrets](/docs/tools/admin#secrets).
120
+ description:
121
+ Defines a secret for this microservice. For more information see [Admin Secrets](/docs/tools/admin#secrets).
122
+ Note that unlike the INTERFACE SECRET, the microservice SECRET does not take an Option Name parameter,
123
+ because the secret value is injected directly into the microservice process as an environment variable
124
+ (Type ENV) or written to a file (Type FILE).
121
125
  since: 5.3.0
122
126
  parameters:
123
127
  - name: Type
@@ -159,7 +159,8 @@ NAMED_WIDGET:
159
159
  - name: Widget Name
160
160
  required: true
161
161
  description: The unique name applied to the following widget instance.
162
- Names must be unique per screen.
162
+ Names must be unique per screen. Names are case insensitive, e.g.
163
+ getNamedWidget('duration') returns the widget named DURATION.
163
164
  values: .+
164
165
  - name: Widget Type
165
166
  required: true
@@ -81,6 +81,13 @@ TARGET:
81
81
  description:
82
82
  Sets the retention time directly on QuestDB tables for automatic data expiration.
83
83
  QuestDB will automatically remove data older than this retention time.
84
+ Tables are partitioned by day and QuestDB only drops whole partitions, so the
85
+ effective granularity is one day. Hour values are rounded up to whole days
86
+ ("1h" becomes 1 day, "25h" becomes 2 days) so data is never dropped early,
87
+ and data is not removed until the entire day partition is older than the retention
88
+ time (expect up to a day of extra data). Expiration is evaluated when new data is
89
+ written to the table, so an idle table is not expired.
90
+ Changing this value and reinstalling the plugin updates existing tables.
84
91
  since: 7.0.0
85
92
  parameters:
86
93
  - name: Time
@@ -95,6 +102,13 @@ TARGET:
95
102
  description:
96
103
  Sets the retention time directly on QuestDB tables for automatic data expiration.
97
104
  QuestDB will automatically remove data older than this retention time.
105
+ Tables are partitioned by day and QuestDB only drops whole partitions, so the
106
+ effective granularity is one day. Hour values are rounded up to whole days
107
+ ("1h" becomes 1 day, "25h" becomes 2 days) so data is never dropped early,
108
+ and data is not removed until the entire day partition is older than the retention
109
+ time (expect up to a day of extra data). Expiration is evaluated when new data is
110
+ written to the table, so an idle table is not expired.
111
+ Changing this value and reinstalling the plugin updates existing tables.
98
112
  since: 7.0.0
99
113
  parameters:
100
114
  - name: Time
@@ -104,6 +118,13 @@ TARGET:
104
118
  Supported units are h (hours), d (days), w (weeks), M (months), y (years).
105
119
  Default = nil = Forever
106
120
  values: \d+[hdwMy]
121
+ DECOM_FLUSH_PERIOD:
122
+ summary: Period in seconds between flushing rows to the TSDB. Higher values use less CPU.
123
+ parameters:
124
+ - name: Period
125
+ required: true
126
+ description: Number of seconds between flushing rows to the TSDB (default = 5.0)
127
+ values: .+
107
128
  LOG_RETAIN_TIME:
108
129
  summary: How long to keep all regular telemetry logs in seconds.
109
130
  parameters:
@@ -170,3 +191,20 @@ TARGET:
170
191
  values: \d+
171
192
  example: |
172
193
  DB_SHARD 0
194
+ STORED_LIMITS_MODE:
195
+ summary: Controls how limits are evaluated for stored (non-real-time) telemetry packets
196
+ description:
197
+ Sets the limits handling policy for packets where the stored flag is true
198
+ (e.g., packets from file interfaces or historical data replay).
199
+ PROCESS processes limits normally including logging and reactions.
200
+ LOG evaluates limits and logs state changes but does not trigger limits reactions
201
+ or update the current limits state used by the API.
202
+ DISABLE skips limits processing entirely for stored packets.
203
+ since: 7.2.1
204
+ parameters:
205
+ - name: Mode
206
+ required: true
207
+ description: "PROCESS (default), LOG, or DISABLE"
208
+ values: ^(PROCESS|LOG|DISABLE)$
209
+ example: |
210
+ STORED_LIMITS_MODE DISABLE
@@ -2,7 +2,9 @@
2
2
  TOOL:
3
3
  summary: Define a tool
4
4
  example: TOOL DEMO Demo
5
- description: Defines a tool that the plugin adds to the OpenC3 system. Tools are web based applications that make use of the Single-SPA javascript library that allows them to by dynamically added to the running system as independent frontend microservices.
5
+ description: |
6
+ Defines a tool that the plugin adds to the OpenC3 system. Tools are web based applications that make use of the Single-SPA javascript library that allows them to be dynamically added to the running system as independent frontend microservices.
7
+ Tools are global to the entire COSMOS installation. Regardless of the scope a plugin is installed into, its tools are always installed into the DEFAULT scope and are available in every scope.
6
8
  parameters:
7
9
  - name: Tool Folder Name
8
10
  description: The exact name of the tool folder in the plugin. ie. tools/ToolFolderName