openc3 7.2.1 → 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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +97 -6
  3. data/bin/pipinstall +5 -4
  4. data/bin/pipuninstall +5 -1
  5. data/bin/uvinstall +149 -0
  6. data/data/config/item_modifiers.yaml +2 -1
  7. data/data/config/microservice.yaml +5 -1
  8. data/data/config/screen.yaml +2 -1
  9. data/data/config/target.yaml +21 -0
  10. data/data/config/tool.yaml +3 -1
  11. data/data/config/widgets.yaml +16 -3
  12. data/data/xtce_schemas/SpaceSystem_20180204.xsd +5918 -0
  13. data/data/xtce_schemas/xml.xsd +116 -0
  14. data/ext/openc3/ext/telemetry/telemetry.c +1 -1
  15. data/lib/openc3/accessors/template_accessor.rb +2 -2
  16. data/lib/openc3/api/interface_api.rb +2 -2
  17. data/lib/openc3/api/limits_api.rb +27 -13
  18. data/lib/openc3/api/router_api.rb +3 -3
  19. data/lib/openc3/api/tlm_api.rb +1 -1
  20. data/lib/openc3/interfaces/http_client_interface.rb +1 -1
  21. data/lib/openc3/io/json_drb.rb +24 -8
  22. data/lib/openc3/io/json_rpc.rb +0 -2
  23. data/lib/openc3/logs/log_writer.rb +43 -7
  24. data/lib/openc3/logs/packet_log_writer.rb +1 -2
  25. data/lib/openc3/microservices/interface_microservice.rb +1 -1
  26. data/lib/openc3/migrations/20260731000000_system_health_json.rb +41 -0
  27. data/lib/openc3/models/cvt_model.rb +5 -5
  28. data/lib/openc3/models/gem_model.rb +1 -1
  29. data/lib/openc3/models/interface_model.rb +1 -1
  30. data/lib/openc3/models/microservice_model.rb +21 -0
  31. data/lib/openc3/models/plugin_model.rb +204 -50
  32. data/lib/openc3/models/python_package_model.rb +207 -11
  33. data/lib/openc3/models/scope_model.rb +34 -30
  34. data/lib/openc3/models/target_model.rb +180 -6
  35. data/lib/openc3/models/tool_model.rb +33 -20
  36. data/lib/openc3/operators/microservice_operator.rb +33 -4
  37. data/lib/openc3/packets/commands.rb +4 -4
  38. data/lib/openc3/packets/limits.rb +15 -7
  39. data/lib/openc3/packets/packet.rb +1 -1
  40. data/lib/openc3/packets/parsers/state_parser.rb +1 -1
  41. data/lib/openc3/packets/parsers/xtce_converter.rb +183 -103
  42. data/lib/openc3/packets/parsers/xtce_parser.rb +71 -23
  43. data/lib/openc3/packets/structure.rb +2 -2
  44. data/lib/openc3/packets/telemetry.rb +3 -3
  45. data/lib/openc3/script/commands.rb +1 -1
  46. data/lib/openc3/script/extract.rb +20 -25
  47. data/lib/openc3/script/web_socket_api.rb +44 -4
  48. data/lib/openc3/topics/command_decom_topic.rb +16 -1
  49. data/lib/openc3/topics/limits_event_topic.rb +1 -1
  50. data/lib/openc3/utilities/aws_bucket.rb +10 -2
  51. data/lib/openc3/utilities/csv.rb +5 -5
  52. data/lib/openc3/utilities/local_mode.rb +4 -1
  53. data/lib/openc3/utilities/ruby_lex_utils.rb +5 -1
  54. data/lib/openc3/utilities/running_script.rb +145 -90
  55. data/lib/openc3/utilities/script.rb +46 -10
  56. data/lib/openc3/version.rb +6 -6
  57. data/templates/plugin/plugin.gemspec +1 -1
  58. data/templates/tool_angular/package.json +2 -2
  59. data/templates/tool_react/package.json +1 -1
  60. data/templates/tool_svelte/package.json +1 -1
  61. data/templates/tool_vue/package.json +3 -3
  62. data/templates/widget/package.json +2 -2
  63. metadata +5 -3
  64. data/templates/tool_vue/.nycrc +0 -3
  65. data/templates/widget/.nycrc +0 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13ecd5ed69a40943e0ceb4732af0922106d73ba3e28c7e10521e3cbcbb2728c9
4
- data.tar.gz: e8a55a75e964de92cdc8fa9a27091d472437004a0775a05c894d4a16d5232677
3
+ metadata.gz: 787269f06fae8562e23d790d3c4887e2f913841dc818570fdafa06b29fd1b8e6
4
+ data.tar.gz: c6630dbc9c61bf71f96015db09ca9246ef80247f4f1b5de429c97b537551c4e4
5
5
  SHA512:
6
- metadata.gz: 77c7ed5bcae0862bf8d7ccdeefbc79cdc814b04501831e5de5a2b7b1d685420f731da2bb230435a1d091697156ab1d58a40cf793869da48e191b7c87ccbffea9
7
- data.tar.gz: 90571e3012ae117610314df3d32d67d77a843f2de8edb2c9b51bab3335cf0618e7301a9ba0c55616acad6a2d0ca1b4634924531f657bd438c0fd490aeca6a327
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}")
@@ -1463,6 +1523,37 @@ if not ARGV[0].nil? # argument(s) given
1463
1523
  when 'migratepassword'
1464
1524
  migrate_password_hash()
1465
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
+
1466
1557
  else # Unknown task
1467
1558
  print_usage()
1468
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
@@ -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:
@@ -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
@@ -1442,6 +1442,19 @@ Interactive Widgets:
1442
1442
  Scripts can be launched from a BUTTON using the `runScript()` method. `runScript()` takes three parameters,
1443
1443
  the name of the script, whether to open the script in the foreground of Script Runner (default = true), and a hash of
1444
1444
  environment variables. For example: `runScript('INST/procedures/script.rb', false, {'VAR': 'VALUE'})`
1445
+
1446
+ For security, button code does NOT run in the main application. It runs in an isolated,
1447
+ sandboxed browser context that has no access to your login session, browser storage, or
1448
+ the page, and no network access of its own. The `api`, `screen`, `runScript` and `alert`
1449
+ objects still work exactly as before (the real work is performed by the application on the
1450
+ button code's behalf). A few consequences of this isolation:
1451
+
1452
+ - `alert()` no longer pauses code execution while the alert is displayed.
1453
+ - `screen.getNamedWidget("WIDGET_NAME").text()` (and `selected()` / `checked()` / `value`)
1454
+ return the widget's value as it was when the button was clicked; a `.value =` assignment
1455
+ made earlier in the same button click is not guaranteed to be visible to a later read in
1456
+ that same click.
1457
+ - The `self` variable (the internal widget component) is no longer available to button code.
1445
1458
  parameters:
1446
1459
  - name: Button Text
1447
1460
  required: true
@@ -1613,7 +1626,7 @@ Canvas Widgets:
1613
1626
  description: Blue value of the RGB value
1614
1627
  values: .+
1615
1628
  example: |
1616
- CANVAS 100 50
1629
+ CANVAS 150 50
1617
1630
  SETTING BACKCOLOR 17 21 28
1618
1631
  CANVASLABEL 5 30 "Dark canvas" 18 white
1619
1632
  END
@@ -1682,8 +1695,8 @@ Canvas Widgets:
1682
1695
  values: <%= %w(RAW CONVERTED FORMATTED) %>
1683
1696
  example: |
1684
1697
  CANVAS 200 100
1685
- CANVASLABELVALUE INST HEALTH_STATUS TEMP1 5 34 12 red
1686
- CANVASLABELVALUE INST HEALTH_STATUS TEMP2 5 70 10 blue FORMATTED
1698
+ CANVASLABELVALUE INST HEALTH_STATUS TEMP1 5 34 20 red
1699
+ CANVASLABELVALUE INST HEALTH_STATUS TEMP2 5 70 16 blue FORMATTED
1687
1700
  END
1688
1701
  CANVASIMAGE:
1689
1702
  summary: Displays an image on the canvas