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.
- checksums.yaml +4 -4
- data/bin/openc3cli +97 -6
- data/bin/pipinstall +5 -4
- data/bin/pipuninstall +5 -1
- data/bin/uvinstall +149 -0
- data/data/config/item_modifiers.yaml +2 -1
- data/data/config/microservice.yaml +5 -1
- data/data/config/screen.yaml +2 -1
- data/data/config/target.yaml +21 -0
- data/data/config/tool.yaml +3 -1
- data/data/config/widgets.yaml +16 -3
- data/data/xtce_schemas/SpaceSystem_20180204.xsd +5918 -0
- data/data/xtce_schemas/xml.xsd +116 -0
- data/ext/openc3/ext/telemetry/telemetry.c +1 -1
- data/lib/openc3/accessors/template_accessor.rb +2 -2
- data/lib/openc3/api/interface_api.rb +2 -2
- data/lib/openc3/api/limits_api.rb +27 -13
- data/lib/openc3/api/router_api.rb +3 -3
- data/lib/openc3/api/tlm_api.rb +1 -1
- data/lib/openc3/interfaces/http_client_interface.rb +1 -1
- data/lib/openc3/io/json_drb.rb +24 -8
- data/lib/openc3/io/json_rpc.rb +0 -2
- data/lib/openc3/logs/log_writer.rb +43 -7
- data/lib/openc3/logs/packet_log_writer.rb +1 -2
- data/lib/openc3/microservices/interface_microservice.rb +1 -1
- data/lib/openc3/migrations/20260731000000_system_health_json.rb +41 -0
- data/lib/openc3/models/cvt_model.rb +5 -5
- data/lib/openc3/models/gem_model.rb +1 -1
- data/lib/openc3/models/interface_model.rb +1 -1
- data/lib/openc3/models/microservice_model.rb +21 -0
- data/lib/openc3/models/plugin_model.rb +204 -50
- data/lib/openc3/models/python_package_model.rb +207 -11
- data/lib/openc3/models/scope_model.rb +34 -30
- data/lib/openc3/models/target_model.rb +180 -6
- data/lib/openc3/models/tool_model.rb +33 -20
- data/lib/openc3/operators/microservice_operator.rb +33 -4
- data/lib/openc3/packets/commands.rb +4 -4
- data/lib/openc3/packets/limits.rb +15 -7
- data/lib/openc3/packets/packet.rb +1 -1
- data/lib/openc3/packets/parsers/state_parser.rb +1 -1
- data/lib/openc3/packets/parsers/xtce_converter.rb +183 -103
- data/lib/openc3/packets/parsers/xtce_parser.rb +71 -23
- data/lib/openc3/packets/structure.rb +2 -2
- data/lib/openc3/packets/telemetry.rb +3 -3
- data/lib/openc3/script/commands.rb +1 -1
- data/lib/openc3/script/extract.rb +20 -25
- data/lib/openc3/script/web_socket_api.rb +44 -4
- data/lib/openc3/topics/command_decom_topic.rb +16 -1
- data/lib/openc3/topics/limits_event_topic.rb +1 -1
- data/lib/openc3/utilities/aws_bucket.rb +10 -2
- data/lib/openc3/utilities/csv.rb +5 -5
- data/lib/openc3/utilities/local_mode.rb +4 -1
- data/lib/openc3/utilities/ruby_lex_utils.rb +5 -1
- data/lib/openc3/utilities/running_script.rb +145 -90
- data/lib/openc3/utilities/script.rb +46 -10
- data/lib/openc3/version.rb +6 -6
- data/templates/plugin/plugin.gemspec +1 -1
- data/templates/tool_angular/package.json +2 -2
- data/templates/tool_react/package.json +1 -1
- data/templates/tool_svelte/package.json +1 -1
- data/templates/tool_vue/package.json +3 -3
- data/templates/widget/package.json +2 -2
- metadata +5 -3
- data/templates/tool_vue/.nycrc +0 -3
- data/templates/widget/.nycrc +0 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 787269f06fae8562e23d790d3c4887e2f913841dc818570fdafa06b29fd1b8e6
|
|
4
|
+
data.tar.gz: c6630dbc9c61bf71f96015db09ca9246ef80247f4f1b5de429c97b537551c4e4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
132
|
-
options[:time_association_name] =
|
|
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
|
-
|
|
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
|
-
|
|
2
|
+
VENV="${PIPINSTALL_VENV:-$PYTHONUSERBASE}"
|
|
3
|
+
uv venv "$VENV" --allow-existing
|
|
3
4
|
echo "uv pip install $@"
|
|
4
|
-
uv pip install --python "$
|
|
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 "$
|
|
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 "$
|
|
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 "$
|
|
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:
|
|
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
|
data/data/config/screen.yaml
CHANGED
|
@@ -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
|
data/data/config/target.yaml
CHANGED
|
@@ -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:
|
data/data/config/tool.yaml
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
TOOL:
|
|
3
3
|
summary: Define a tool
|
|
4
4
|
example: TOOL DEMO Demo
|
|
5
|
-
description:
|
|
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
|
data/data/config/widgets.yaml
CHANGED
|
@@ -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
|
|
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
|
|
1686
|
-
CANVASLABELVALUE INST HEALTH_STATUS TEMP2 5 70
|
|
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
|