openc3 7.2.0 → 7.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/bin/openc3cli +11 -23
- data/data/config/interface_modifiers.yaml +15 -0
- data/data/config/target.yaml +17 -0
- data/data/config/widgets.yaml +28 -1
- data/lib/openc3/accessors/binary_accessor.rb +4 -0
- data/lib/openc3/api/cmd_api.rb +5 -1
- data/lib/openc3/api/limits_api.rb +72 -0
- data/lib/openc3/microservices/decom_common.rb +15 -4
- data/lib/openc3/microservices/decom_microservice.rb +6 -1
- data/lib/openc3/microservices/interface_microservice.rb +25 -11
- data/lib/openc3/microservices/queue_microservice.rb +4 -2
- data/lib/openc3/migrations/20260701000000_updated_at_to_int.rb +55 -0
- data/lib/openc3/models/cvt_model.rb +2 -2
- data/lib/openc3/models/db_sharded_model.rb +2 -2
- data/lib/openc3/models/interface_model.rb +16 -10
- data/lib/openc3/models/interface_status_model.rb +2 -2
- data/lib/openc3/models/metric_model.rb +2 -2
- data/lib/openc3/models/microservice_status_model.rb +2 -2
- data/lib/openc3/models/model.rb +2 -6
- data/lib/openc3/models/plugin_model.rb +15 -14
- data/lib/openc3/models/python_package_model.rb +3 -1
- data/lib/openc3/models/reingest_job_model.rb +1 -1
- data/lib/openc3/models/script_status_model.rb +73 -38
- data/lib/openc3/models/target_model.rb +13 -0
- data/lib/openc3/operators/operator.rb +15 -5
- data/lib/openc3/packets/commands.rb +3 -3
- data/lib/openc3/packets/limits.rb +23 -0
- data/lib/openc3/packets/packet.rb +5 -3
- data/lib/openc3/packets/packet_config.rb +5 -1
- data/lib/openc3/packets/structure.rb +14 -10
- data/lib/openc3/script/limits.rb +1 -1
- data/lib/openc3/script/suite_runner.rb +43 -9
- data/lib/openc3/script/web_socket_api.rb +1 -1
- data/lib/openc3/system/target.rb +4 -1
- data/lib/openc3/tools/table_manager/table_config.rb +4 -1
- data/lib/openc3/top_level.rb +1 -1
- data/lib/openc3/topics/limits_event_topic.rb +62 -3
- data/lib/openc3/topics/telemetry_decom_topic.rb +2 -2
- data/lib/openc3/utilities/cli_generator.rb +5 -1
- data/lib/openc3/utilities/env_helper.rb +13 -1
- data/lib/openc3/utilities/pypi_url.rb +39 -0
- data/lib/openc3/utilities/reingest_job.rb +1 -1
- data/lib/openc3/utilities/running_script.rb +17 -12
- data/lib/openc3/utilities/script.rb +14 -3
- data/lib/openc3/utilities/store_autoload.rb +39 -1
- data/lib/openc3/utilities/store_queued.rb +5 -1
- data/lib/openc3/version.rb +5 -5
- data/lib/openc3.rb +6 -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 +3 -1
data/lib/openc3/models/model.rb
CHANGED
|
@@ -134,7 +134,7 @@ module OpenC3
|
|
|
134
134
|
|
|
135
135
|
# Update the Redis hash at primary_key and set the field "name"
|
|
136
136
|
# to the JSON generated via calling as_json
|
|
137
|
-
def create(update: false, force: false, queued: false
|
|
137
|
+
def create(update: false, force: false, queued: false)
|
|
138
138
|
unless force
|
|
139
139
|
existing = self.class.store.hget(@primary_key, @name)
|
|
140
140
|
if existing
|
|
@@ -143,11 +143,7 @@ module OpenC3
|
|
|
143
143
|
raise "#{@primary_key}:#{@name} doesn't exist at update" if update
|
|
144
144
|
end
|
|
145
145
|
end
|
|
146
|
-
|
|
147
|
-
@updated_at = Time.now.utc.iso8601
|
|
148
|
-
else
|
|
149
|
-
@updated_at = Time.now.utc.to_nsec_from_epoch
|
|
150
|
-
end
|
|
146
|
+
@updated_at = Time.now.utc.to_nsec_from_epoch
|
|
151
147
|
|
|
152
148
|
if queued
|
|
153
149
|
write_store = self.class.store_queued
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# encoding:
|
|
1
|
+
# encoding: utf-8
|
|
2
2
|
|
|
3
3
|
# Copyright 2022 Ball Aerospace & Technologies Corp.
|
|
4
4
|
# All Rights Reserved.
|
|
@@ -31,9 +31,11 @@ require 'openc3/models/tool_model'
|
|
|
31
31
|
require 'openc3/models/widget_model'
|
|
32
32
|
require 'openc3/models/microservice_model'
|
|
33
33
|
require 'openc3/api/api'
|
|
34
|
+
require 'openc3/utilities/pypi_url'
|
|
34
35
|
require 'tmpdir'
|
|
35
36
|
require 'tempfile'
|
|
36
37
|
require 'fileutils'
|
|
38
|
+
require 'open3'
|
|
37
39
|
|
|
38
40
|
module OpenC3
|
|
39
41
|
class EmptyGemFileError < StandardError; end
|
|
@@ -262,32 +264,31 @@ module OpenC3
|
|
|
262
264
|
if pypi_url
|
|
263
265
|
pypi_url += '/simple'
|
|
264
266
|
end
|
|
265
|
-
pypi_url ||=
|
|
267
|
+
pypi_url ||= PypiUrl::DEFAULT
|
|
266
268
|
end
|
|
267
269
|
end
|
|
270
|
+
pypi_url = PypiUrl.validate(pypi_url)
|
|
268
271
|
unless validate_only
|
|
272
|
+
# Build the pip arguments as an argv array and run pipinstall without a
|
|
273
|
+
# shell. pypi_url comes from a user-writable setting, so interpolating it
|
|
274
|
+
# into a shell command line allowed OS command injection. An argv array
|
|
275
|
+
# passes each argument verbatim with no shell metacharacter interpretation.
|
|
276
|
+
pip_args = ["-i", pypi_url]
|
|
277
|
+
pip_args += ["--trusted-host", URI.parse(pypi_url).host] unless ENV['PIP_ENABLE_TRUSTED_HOST'].nil?
|
|
269
278
|
if File.exist?(pyproject_path)
|
|
270
279
|
Logger.info "Installing python packages from pyproject.toml with pypi_url=#{pypi_url}"
|
|
271
|
-
|
|
272
|
-
pip_args = "-i #{pypi_url} #{gem_path}"
|
|
273
|
-
else
|
|
274
|
-
pip_args = "-i #{pypi_url} --trusted-host #{URI.parse(pypi_url).host} #{gem_path}"
|
|
275
|
-
end
|
|
280
|
+
pip_args << gem_path
|
|
276
281
|
else
|
|
277
282
|
Logger.info "Installing python packages from requirements.txt with pypi_url=#{pypi_url}"
|
|
278
|
-
|
|
279
|
-
pip_args = "-i #{pypi_url} -r #{requirements_path}"
|
|
280
|
-
else
|
|
281
|
-
pip_args = "-i #{pypi_url} --trusted-host #{URI.parse(pypi_url).host} -r #{requirements_path}"
|
|
282
|
-
end
|
|
283
|
+
pip_args += ["-r", requirements_path]
|
|
283
284
|
end
|
|
284
285
|
# Capture output and check exit code so failures surface as a warning
|
|
285
286
|
# rather than silently succeeding. pipinstall is non-fatal: the plugin
|
|
286
287
|
# continues to install even if Python packages fail so that non-Python
|
|
287
288
|
# functionality still works.
|
|
288
|
-
output =
|
|
289
|
+
output, status = Open3.capture2e("/openc3/bin/pipinstall", *pip_args)
|
|
289
290
|
puts output
|
|
290
|
-
unless
|
|
291
|
+
unless status.success?
|
|
291
292
|
Logger.warn "Python package installation failed. Plugin Python microservices may not function correctly."
|
|
292
293
|
end
|
|
293
294
|
end
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
require 'fileutils'
|
|
15
15
|
require 'openc3/utilities/process_manager'
|
|
16
|
+
require 'openc3/utilities/pypi_url'
|
|
16
17
|
require 'openc3/api/api'
|
|
17
18
|
require 'pathname'
|
|
18
19
|
|
|
@@ -83,9 +84,10 @@ module OpenC3
|
|
|
83
84
|
if pypi_url
|
|
84
85
|
pypi_url += '/simple'
|
|
85
86
|
end
|
|
86
|
-
pypi_url ||=
|
|
87
|
+
pypi_url ||= PypiUrl::DEFAULT
|
|
87
88
|
end
|
|
88
89
|
end
|
|
90
|
+
pypi_url = PypiUrl.validate(pypi_url)
|
|
89
91
|
Logger.info "Installing python package: #{name_or_path}"
|
|
90
92
|
if ENV['PIP_ENABLE_TRUSTED_HOST'].nil?
|
|
91
93
|
pip_args = ["-i", pypi_url, package_file_path]
|
|
@@ -62,48 +62,83 @@ module OpenC3
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def self.all(scope:, offset: 0, limit: 10, type: "running")
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
result = result.map do |r|
|
|
75
|
-
if r.nil?
|
|
76
|
-
nil
|
|
77
|
-
else
|
|
78
|
-
JSON.parse(r, allow_nan: true, create_additions: true)
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
return result
|
|
82
|
-
else
|
|
83
|
-
keys = self.store.zrevrange("#{COMPLETED_PRIMARY_KEY}__#{scope}__LIST", offset.to_i, offset.to_i + limit.to_i - 1)
|
|
84
|
-
return [] if keys.empty?
|
|
85
|
-
result = []
|
|
86
|
-
result = self.store.redis_pool.pipelined do
|
|
87
|
-
keys.each do |key|
|
|
88
|
-
self.store.hget("#{COMPLETED_PRIMARY_KEY}__#{scope}", key)
|
|
89
|
-
end
|
|
65
|
+
primary_key = (type == "running") ? RUNNING_PRIMARY_KEY : COMPLETED_PRIMARY_KEY
|
|
66
|
+
keys = self.store.zrevrange("#{primary_key}__#{scope}__LIST", offset.to_i, offset.to_i + limit.to_i - 1)
|
|
67
|
+
return [] if keys.empty?
|
|
68
|
+
|
|
69
|
+
result = self.store.redis_pool.pipelined do
|
|
70
|
+
keys.each do |key|
|
|
71
|
+
self.store.hget("#{primary_key}__#{scope}", key)
|
|
90
72
|
end
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
73
|
+
end
|
|
74
|
+
result = result.map do |r|
|
|
75
|
+
if r.nil?
|
|
76
|
+
nil
|
|
77
|
+
else
|
|
78
|
+
JSON.parse(r, allow_nan: true, create_additions: true)
|
|
97
79
|
end
|
|
98
|
-
return result
|
|
99
80
|
end
|
|
81
|
+
return result
|
|
100
82
|
end
|
|
101
83
|
|
|
102
84
|
def self.count(scope:, type: "running")
|
|
103
|
-
|
|
104
|
-
|
|
85
|
+
primary_key = (type == "running") ? RUNNING_PRIMARY_KEY : COMPLETED_PRIMARY_KEY
|
|
86
|
+
return self.store.zcount("#{primary_key}__#{scope}__LIST", 0, Float::INFINITY)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Fields that a search term is matched against (case-insensitive substring)
|
|
90
|
+
SEARCH_FIELDS = ['name', 'filename', 'username', 'user_full_name', 'state']
|
|
91
|
+
|
|
92
|
+
# Return a single page of items along with the total count of matching items.
|
|
93
|
+
# When searching, this fetches, parses, and filters the full list only once
|
|
94
|
+
# (instead of doing that work separately for the page and the count).
|
|
95
|
+
# Returns [page_items, total]
|
|
96
|
+
def self.page(scope:, offset: 0, limit: 10, type: "running", search: nil)
|
|
97
|
+
primary_key = (type == "running") ? RUNNING_PRIMARY_KEY : COMPLETED_PRIMARY_KEY
|
|
98
|
+
if search and !search.to_s.empty?
|
|
99
|
+
items = self.filter_by_search(self.fetch_all(primary_key, scope), search)
|
|
100
|
+
return [items[offset.to_i, limit.to_i] || [], items.length]
|
|
105
101
|
else
|
|
106
|
-
|
|
102
|
+
total = self.store.zcount("#{primary_key}__#{scope}__LIST", 0, Float::INFINITY)
|
|
103
|
+
keys = self.store.zrevrange("#{primary_key}__#{scope}__LIST", offset.to_i, offset.to_i + limit.to_i - 1)
|
|
104
|
+
page_items = keys.empty? ? [] : self.fetch(primary_key, scope, keys)
|
|
105
|
+
return [page_items, total]
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Fetch and parse the hash values for the given keys, preserving their order
|
|
110
|
+
def self.fetch(primary_key, scope, keys)
|
|
111
|
+
result = self.store.redis_pool.pipelined do
|
|
112
|
+
keys.each do |key|
|
|
113
|
+
self.store.hget("#{primary_key}__#{scope}", key)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
result.map do |r|
|
|
117
|
+
# NOTE: create_additions is intentionally omitted here. ScriptStatusModel
|
|
118
|
+
# records are written via JSON.generate of as_json (plain hashes/arrays/
|
|
119
|
+
# primitives) and never contain a "json_class" marker, so create_additions
|
|
120
|
+
# has nothing to reconstruct. Enabling it disables the parser's fast path
|
|
121
|
+
# and makes this parse ~3x slower - costly since this runs over every
|
|
122
|
+
# record when listing/searching.
|
|
123
|
+
r.nil? ? nil : JSON.parse(r, allow_nan: true)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Fetch and parse every item for the type, in list order (most recent first)
|
|
128
|
+
def self.fetch_all(primary_key, scope)
|
|
129
|
+
keys = self.store.zrevrange("#{primary_key}__#{scope}__LIST", 0, -1)
|
|
130
|
+
return [] if keys.empty?
|
|
131
|
+
|
|
132
|
+
self.fetch(primary_key, scope, keys).compact
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Select items where any SEARCH_FIELDS value contains the search term (case-insensitive)
|
|
136
|
+
def self.filter_by_search(items, search)
|
|
137
|
+
search = search.to_s.downcase
|
|
138
|
+
items.select do |item|
|
|
139
|
+
next false if item.nil?
|
|
140
|
+
|
|
141
|
+
SEARCH_FIELDS.any? { |field| item[field].to_s.downcase.include?(search) }
|
|
107
142
|
end
|
|
108
143
|
end
|
|
109
144
|
|
|
@@ -175,7 +210,7 @@ module OpenC3
|
|
|
175
210
|
|
|
176
211
|
# Update the Redis hash at primary_key and set the field "name"
|
|
177
212
|
# to the JSON generated via calling as_json
|
|
178
|
-
def create(update: false, force: false, queued: false
|
|
213
|
+
def create(update: false, force: false, queued: false)
|
|
179
214
|
@updated_at = Time.now.utc.to_nsec_from_epoch
|
|
180
215
|
|
|
181
216
|
if queued
|
|
@@ -198,9 +233,9 @@ module OpenC3
|
|
|
198
233
|
|
|
199
234
|
# Move to completed
|
|
200
235
|
@primary_key = "#{COMPLETED_PRIMARY_KEY}__#{@scope}"
|
|
201
|
-
create(update: false, force: force, queued: queued
|
|
236
|
+
create(update: false, force: force, queued: queued)
|
|
202
237
|
else
|
|
203
|
-
create(update: true, force: force, queued: queued
|
|
238
|
+
create(update: true, force: force, queued: queued)
|
|
204
239
|
end
|
|
205
240
|
end
|
|
206
241
|
|
|
@@ -63,6 +63,7 @@ module OpenC3
|
|
|
63
63
|
attr_accessor :ignored_parameters
|
|
64
64
|
attr_accessor :ignored_items
|
|
65
65
|
attr_accessor :limits_groups
|
|
66
|
+
attr_accessor :stored_limits_mode
|
|
66
67
|
attr_accessor :cmd_tlm_files
|
|
67
68
|
attr_accessor :id
|
|
68
69
|
attr_accessor :cmd_buffer_depth
|
|
@@ -384,6 +385,7 @@ module OpenC3
|
|
|
384
385
|
ignored_parameters: [],
|
|
385
386
|
ignored_items: [],
|
|
386
387
|
limits_groups: [],
|
|
388
|
+
stored_limits_mode: 'PROCESS',
|
|
387
389
|
cmd_tlm_files: [],
|
|
388
390
|
id: nil,
|
|
389
391
|
updated_at: nil,
|
|
@@ -412,6 +414,8 @@ module OpenC3
|
|
|
412
414
|
@ignored_parameters = ignored_parameters
|
|
413
415
|
@ignored_items = ignored_items
|
|
414
416
|
@limits_groups = limits_groups
|
|
417
|
+
@stored_limits_mode = stored_limits_mode.to_s.upcase
|
|
418
|
+
@stored_limits_mode = 'PROCESS' unless %w(PROCESS LOG DISABLE).include?(@stored_limits_mode)
|
|
415
419
|
@cmd_tlm_files = cmd_tlm_files
|
|
416
420
|
@id = id
|
|
417
421
|
@cmd_buffer_depth = cmd_buffer_depth
|
|
@@ -442,6 +446,7 @@ module OpenC3
|
|
|
442
446
|
'ignored_parameters' => @ignored_parameters,
|
|
443
447
|
'ignored_items' => @ignored_items,
|
|
444
448
|
'limits_groups' => @limits_groups,
|
|
449
|
+
'stored_limits_mode' => @stored_limits_mode,
|
|
445
450
|
'cmd_tlm_files' => @cmd_tlm_files,
|
|
446
451
|
'id' => @id,
|
|
447
452
|
'updated_at' => @updated_at,
|
|
@@ -557,6 +562,14 @@ module OpenC3
|
|
|
557
562
|
parser.verify_num_parameters(1, 1, "#{keyword} <Shard Number Starting from 0>")
|
|
558
563
|
@db_shard = Integer(parameters[0])
|
|
559
564
|
|
|
565
|
+
when 'STORED_LIMITS_MODE'
|
|
566
|
+
parser.verify_num_parameters(1, 1, "#{keyword} <PROCESS, LOG, or DISABLE>")
|
|
567
|
+
mode = parameters[0].to_s.upcase
|
|
568
|
+
unless %w(PROCESS LOG DISABLE).include?(mode)
|
|
569
|
+
raise ConfigParser::Error.new(parser, "STORED_LIMITS_MODE must be one of PROCESS, LOG, or DISABLE")
|
|
570
|
+
end
|
|
571
|
+
@stored_limits_mode = mode
|
|
572
|
+
|
|
560
573
|
else
|
|
561
574
|
raise ConfigParser::Error.new(parser, "Unknown keyword and parameters for Target: #{keyword} #{parameters.join(" ")}")
|
|
562
575
|
end
|
|
@@ -381,11 +381,21 @@ module OpenC3
|
|
|
381
381
|
# Monitor processes and respawn if died
|
|
382
382
|
Logger.info("#{self.class} Monitoring processes every #{@cycle_time} sec...")
|
|
383
383
|
loop do
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
384
|
+
# A single cycle must never be able to take down the operator process.
|
|
385
|
+
# update() in particular hits Redis every cycle and a transient network
|
|
386
|
+
# error (the kind that also makes targets reconnect) would otherwise
|
|
387
|
+
# unwind run() and exit the process, which in the container looks like a
|
|
388
|
+
# full operator restart. Catch, log, and keep cycling so the next cycle
|
|
389
|
+
# can recover once Redis is reachable again.
|
|
390
|
+
begin
|
|
391
|
+
update()
|
|
392
|
+
remove_old()
|
|
393
|
+
respawn_changed()
|
|
394
|
+
start_new()
|
|
395
|
+
respawn_dead()
|
|
396
|
+
rescue => e
|
|
397
|
+
Logger.error("#{self.class} cycle error, continuing: #{e.class} #{e.message}\n#{e.backtrace.join("\n")}")
|
|
398
|
+
end
|
|
389
399
|
break if @shutdown
|
|
390
400
|
|
|
391
401
|
sleep(@cycle_time)
|
|
@@ -325,10 +325,10 @@ module OpenC3
|
|
|
325
325
|
unless item.states.values.include?(value)
|
|
326
326
|
if command.raw
|
|
327
327
|
# Raw commands report missing value maps
|
|
328
|
-
raise "Command parameter '#{command.target_name} #{command.packet_name} #{item_upcase}' = #{value.to_s.upcase} not one of #{item.states.values.join(', ')}"
|
|
328
|
+
raise RangeError, "Command parameter '#{command.target_name} #{command.packet_name} #{item_upcase}' = #{value.to_s.upcase} not one of #{item.states.values.join(', ')}"
|
|
329
329
|
else
|
|
330
330
|
# Normal commands report missing state maps
|
|
331
|
-
raise "Command parameter '#{command.target_name} #{command.packet_name} #{item_upcase}' = #{value.to_s.upcase} not one of #{item.states.keys.join(', ')}"
|
|
331
|
+
raise RangeError, "Command parameter '#{command.target_name} #{command.packet_name} #{item_upcase}' = #{value.to_s.upcase} not one of #{item.states.keys.join(', ')}"
|
|
332
332
|
end
|
|
333
333
|
end
|
|
334
334
|
end
|
|
@@ -340,7 +340,7 @@ module OpenC3
|
|
|
340
340
|
# Perform Range Check on command parameter
|
|
341
341
|
if not range.include?(range_check_value)
|
|
342
342
|
range_check_value = "'#{range_check_value}'" if String === range_check_value
|
|
343
|
-
raise "Command parameter '#{command.target_name} #{command.packet_name} #{item_upcase}' = #{range_check_value} not in valid range of #{range.first} to #{range.last}"
|
|
343
|
+
raise RangeError, "Command parameter '#{command.target_name} #{command.packet_name} #{item_upcase}' = #{range_check_value} not in valid range of #{range.first} to #{range.last}"
|
|
344
344
|
end
|
|
345
345
|
end
|
|
346
346
|
end
|
|
@@ -153,6 +153,29 @@ module OpenC3
|
|
|
153
153
|
return [limits_set, limits.persistence_setting, limits.enabled, limits_for_set[0], limits_for_set[1], limits_for_set[2], limits_for_set[3], limits_for_set[4], limits_for_set[5]]
|
|
154
154
|
end
|
|
155
155
|
|
|
156
|
+
# Set the color for a telemetry item state
|
|
157
|
+
#
|
|
158
|
+
# @param target_name [String] Target Name
|
|
159
|
+
# @param packet_name [String] Packet Name
|
|
160
|
+
# @param item_name [String] Item Name
|
|
161
|
+
# @param state_name [String] State Name (e.g. 'CONNECTED')
|
|
162
|
+
# @param color [String or Symbol] New color: GREEN, YELLOW, or RED
|
|
163
|
+
# @return [Symbol] The color that was set
|
|
164
|
+
def set_state_color(target_name, packet_name, item_name, state_name, color)
|
|
165
|
+
packet = _get_packet(target_name, packet_name)
|
|
166
|
+
item = packet.get_item(item_name)
|
|
167
|
+
raise "Item #{target_name} #{packet_name} #{item_name} does not have any states" unless item.states
|
|
168
|
+
state_name = state_name.to_s.upcase
|
|
169
|
+
raise "State #{state_name} does not exist for #{target_name} #{packet_name} #{item_name}" unless item.states.key?(state_name)
|
|
170
|
+
color = color.to_s.upcase.intern
|
|
171
|
+
raise "Invalid state color #{color}. Must be one of #{PacketItem::STATE_COLORS.join(' ')}." unless PacketItem::STATE_COLORS.include?(color)
|
|
172
|
+
item.state_colors ||= {}
|
|
173
|
+
item.state_colors[state_name] = color
|
|
174
|
+
item.limits.enabled = true
|
|
175
|
+
packet.update_limits_items_cache(item)
|
|
176
|
+
return color
|
|
177
|
+
end
|
|
178
|
+
|
|
156
179
|
protected
|
|
157
180
|
|
|
158
181
|
def _get_packet(target_name, packet_name)
|
|
@@ -1285,7 +1285,7 @@ module OpenC3
|
|
|
1285
1285
|
config
|
|
1286
1286
|
end
|
|
1287
1287
|
|
|
1288
|
-
def decom
|
|
1288
|
+
def decom(include_limits_states: true)
|
|
1289
1289
|
# Read all the RAW at once because this could be optimized by the accessor
|
|
1290
1290
|
json_hash = read_items(@sorted_items)
|
|
1291
1291
|
|
|
@@ -1301,7 +1301,9 @@ module OpenC3
|
|
|
1301
1301
|
if item.format_string or item.units
|
|
1302
1302
|
json_hash["#{item.name}__F"] = read_item(item, :FORMATTED, @buffer, given_raw)
|
|
1303
1303
|
end
|
|
1304
|
-
|
|
1304
|
+
if include_limits_states
|
|
1305
|
+
limits_state = item.limits.state
|
|
1306
|
+
end
|
|
1305
1307
|
if limits_state
|
|
1306
1308
|
json_hash["#{item.name}__L"] = limits_state
|
|
1307
1309
|
end
|
|
@@ -1551,4 +1553,4 @@ module OpenC3
|
|
|
1551
1553
|
item
|
|
1552
1554
|
end
|
|
1553
1555
|
end
|
|
1554
|
-
end
|
|
1556
|
+
end
|
|
@@ -161,7 +161,11 @@ module OpenC3
|
|
|
161
161
|
process_target_name = process_target_name.upcase
|
|
162
162
|
parser = ConfigParser.new("https://docs.openc3.com/docs")
|
|
163
163
|
parser.instance_variable_set(:@target_name, process_target_name)
|
|
164
|
-
|
|
164
|
+
# run_erb is false: ERB is only rendered once, at plugin install time
|
|
165
|
+
# (TargetModel#deploy), before files are written to the targets bucket. The
|
|
166
|
+
# targets_modified overlay (e.g. dynamically created packets) is never ERB
|
|
167
|
+
# rendered, so it is treated as data here rather than executed as code.
|
|
168
|
+
parser.parse_file(filename, false, true, false) do |keyword, params|
|
|
165
169
|
if @building_generic_conversion
|
|
166
170
|
case keyword
|
|
167
171
|
# Complete a generic conversion
|
|
@@ -604,19 +604,23 @@ module OpenC3
|
|
|
604
604
|
if item.variable_bit_size
|
|
605
605
|
# Bit size is determined by length field
|
|
606
606
|
length_value = self.read(item.variable_bit_size['length_item_name'], :CONVERTED)
|
|
607
|
-
if
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
607
|
+
if length_value
|
|
608
|
+
if (item.data_type == :INT or item.data_type == :UINT) and not item.original_array_size
|
|
609
|
+
case length_value
|
|
610
|
+
when 0
|
|
611
|
+
return 6
|
|
612
|
+
when 1
|
|
613
|
+
return 14
|
|
614
|
+
when 2
|
|
615
|
+
return 30
|
|
616
|
+
else
|
|
617
|
+
return 62
|
|
618
|
+
end
|
|
615
619
|
else
|
|
616
|
-
return
|
|
620
|
+
return (length_value * item.variable_bit_size['length_bits_per_count']) + item.variable_bit_size['length_value_bit_offset']
|
|
617
621
|
end
|
|
618
622
|
else
|
|
619
|
-
|
|
623
|
+
raise "Length value #{item.variable_bit_size['length_item_name']} for item #{item.name} is nil"
|
|
620
624
|
end
|
|
621
625
|
elsif item.original_bit_size <= 0
|
|
622
626
|
# Bit size is full packet length - bits before item + negative bits saved at end
|
data/lib/openc3/script/limits.rb
CHANGED
|
@@ -20,7 +20,7 @@ module OpenC3
|
|
|
20
20
|
private
|
|
21
21
|
|
|
22
22
|
# Define all the modification methods such that we can disconnect them
|
|
23
|
-
%i(enable_limits disable_limits set_limits enable_limits_group disable_limits_group set_limits_set).each do |method_name|
|
|
23
|
+
%i(enable_limits disable_limits set_limits set_state_color enable_limits_group disable_limits_group set_limits_set delete_limits_set).each do |method_name|
|
|
24
24
|
define_method(method_name) do |*args, **kw_args, &block|
|
|
25
25
|
kw_args[:scope] = $openc3_scope unless kw_args[:scope]
|
|
26
26
|
if $disconnect
|
|
@@ -26,6 +26,11 @@ module OpenC3
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
class SuiteRunner
|
|
29
|
+
# Default suite_runner options shared by all callers (e.g. openc3cli) so
|
|
30
|
+
# the values stay consistent in one place.
|
|
31
|
+
DEFAULT_METHOD = 'start'
|
|
32
|
+
DEFAULT_OPTIONS = ['continueAfterError'].freeze
|
|
33
|
+
|
|
29
34
|
@@suites = []
|
|
30
35
|
@@settings = {}
|
|
31
36
|
@@suite_results = nil
|
|
@@ -42,17 +47,46 @@ module OpenC3
|
|
|
42
47
|
@@suite_results
|
|
43
48
|
end
|
|
44
49
|
|
|
50
|
+
# Validate the suite_runner option combinations that don't require the
|
|
51
|
+
# built suites. Shared by all callers (the running script via execute and
|
|
52
|
+
# openc3cli when constructing options) so the contract lives in one place.
|
|
53
|
+
# Suite/Group existence is checked in execute since it needs @@suites.
|
|
54
|
+
def self.validate_options(group: nil, script: nil)
|
|
55
|
+
raise ArgumentError, "Script #{script} requires a Group" if script and not group
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Build the canonical, validated suite_runner hash from raw values,
|
|
59
|
+
# applying defaults. Shared by openc3cli (constructing from CLI flags) and
|
|
60
|
+
# the running script (normalizing a received hash before dispatch) so the
|
|
61
|
+
# hash shape, defaults, and validation live in one place.
|
|
62
|
+
def self.build_options(suite:, group: nil, script: nil, method: nil, options: nil)
|
|
63
|
+
validate_options(group: group, script: script)
|
|
64
|
+
suite_runner = { 'suite' => suite }
|
|
65
|
+
if group
|
|
66
|
+
suite_runner['group'] = group
|
|
67
|
+
suite_runner['script'] = script if script
|
|
68
|
+
end
|
|
69
|
+
# A script always runs via start, matching the GUI
|
|
70
|
+
suite_runner['method'] = script ? DEFAULT_METHOD : (method || DEFAULT_METHOD)
|
|
71
|
+
suite_runner['options'] = options || DEFAULT_OPTIONS.dup
|
|
72
|
+
suite_runner
|
|
73
|
+
end
|
|
74
|
+
|
|
45
75
|
def self.execute(result_string, suite_class, group_class = nil, script = nil)
|
|
46
76
|
@@suite_results = SuiteResults.new
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
77
|
+
# Surface invalid suite / group / option combinations rather than
|
|
78
|
+
# silently running nothing or failing with an opaque nil error. An
|
|
79
|
+
# invalid script method raises later in Group#run_method.
|
|
80
|
+
validate_options(group: group_class, script: script)
|
|
81
|
+
suite = @@suites.find { |s| s.class == suite_class }
|
|
82
|
+
raise ArgumentError, "Suite #{suite_class} not found" unless suite
|
|
83
|
+
if group_class and not suite.scripts.key?(group_class)
|
|
84
|
+
raise ArgumentError, "Group #{group_class} not found in Suite #{suite_class}"
|
|
85
|
+
end
|
|
86
|
+
@@suite_results.start(result_string, suite_class, group_class, script, @@settings)
|
|
87
|
+
loop do
|
|
88
|
+
yield(suite)
|
|
89
|
+
break if not @@settings['Loop'] or (ScriptStatus.instance.fail_count > 0 and @@settings['Break Loop On Error'])
|
|
56
90
|
end
|
|
57
91
|
end
|
|
58
92
|
|
data/lib/openc3/system/target.rb
CHANGED
|
@@ -102,7 +102,10 @@ module OpenC3
|
|
|
102
102
|
def process_file(filename)
|
|
103
103
|
Logger.instance.info "Processing target definition in file '#{filename}'"
|
|
104
104
|
parser = ConfigParser.new("https://docs.openc3.com/docs/configuration/target")
|
|
105
|
-
|
|
105
|
+
# run_erb is false: ERB is only rendered once, at plugin install time
|
|
106
|
+
# (TargetModel#deploy). At runtime target files are read already-rendered
|
|
107
|
+
# from the bucket, so they are treated as data, not re-executed as code.
|
|
108
|
+
parser.parse_file(filename, false, true, false) do |keyword, parameters|
|
|
106
109
|
case keyword
|
|
107
110
|
when 'LANGUAGE'
|
|
108
111
|
usage = "#{keyword} <ruby | python>"
|
|
@@ -85,7 +85,10 @@ module OpenC3
|
|
|
85
85
|
ConfigParser.new(
|
|
86
86
|
'https://openc3.com/docs/tools/#table-manager-configuration-openc3--39',
|
|
87
87
|
)
|
|
88
|
-
|
|
88
|
+
# run_erb is false: ERB is only rendered once, at plugin install time
|
|
89
|
+
# (TargetModel#deploy). Table definitions are read already-rendered from the
|
|
90
|
+
# bucket here, so they are treated as data, not re-executed as code.
|
|
91
|
+
parser.parse_file(filename, false, true, false) do |keyword, params|
|
|
89
92
|
if @building_generic_conversion
|
|
90
93
|
case keyword
|
|
91
94
|
# Complete a generic conversion
|
data/lib/openc3/top_level.rb
CHANGED
|
@@ -108,7 +108,7 @@ module OpenC3
|
|
|
108
108
|
# This method is taken directly from the Rails source:
|
|
109
109
|
# https://api.rubyonrails.org/v5.2/classes/ActiveStorage/Filename.html#method-i-sanitized
|
|
110
110
|
# NOTE: I removed the '/' character because we have to allow this in order to traverse the path
|
|
111
|
-
sanitized = path.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "
|
|
111
|
+
sanitized = path.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "\u{FFFD}").strip.tr("\u{202E}%$|:;\t\r\n\\", "-").gsub('..', '-')
|
|
112
112
|
if sanitized != path
|
|
113
113
|
raise StorageError, "Invalid path: #{path}"
|
|
114
114
|
end
|