openc3 7.0.0.pre.rc3 → 7.0.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/data/config/interface_modifiers.yaml +1 -1
- data/data/config/item_modifiers.yaml +18 -6
- data/data/config/telemetry.yaml +1 -1
- data/lib/openc3/accessors/json_accessor.rb +1 -1
- data/lib/openc3/api/tlm_api.rb +3 -3
- data/lib/openc3/config/config_parser.rb +4 -4
- data/lib/openc3/conversions/conversion.rb +3 -3
- data/lib/openc3/core_ext/faraday.rb +4 -0
- data/lib/openc3/logs/log_writer.rb +24 -6
- data/lib/openc3/logs/packet_log_writer.rb +1 -4
- data/lib/openc3/logs/stream_log_pair.rb +11 -4
- data/lib/openc3/logs/text_log_writer.rb +1 -4
- data/lib/openc3/microservices/interface_microservice.rb +8 -2
- data/lib/openc3/microservices/log_microservice.rb +7 -2
- data/lib/openc3/microservices/microservice.rb +10 -4
- data/lib/openc3/microservices/queue_microservice.rb +3 -0
- data/lib/openc3/microservices/scope_cleanup_microservice.rb +116 -1
- data/lib/openc3/microservices/text_log_microservice.rb +4 -1
- data/lib/openc3/migrations/20260204000000_remove_decom_reducer.rb +2 -0
- data/lib/openc3/models/activity_model.rb +15 -3
- data/lib/openc3/models/cvt_model.rb +2 -247
- data/lib/openc3/models/plugin_store_model.rb +1 -1
- data/lib/openc3/models/script_engine_model.rb +1 -1
- data/lib/openc3/models/target_model.rb +32 -34
- data/lib/openc3/models/tool_model.rb +18 -5
- data/lib/openc3/models/trigger_model.rb +1 -1
- data/lib/openc3/models/widget_model.rb +1 -2
- data/lib/openc3/operators/operator.rb +9 -7
- data/lib/openc3/packets/json_packet.rb +2 -0
- data/lib/openc3/packets/packet.rb +1 -0
- data/lib/openc3/packets/packet_config.rb +28 -12
- data/lib/openc3/script/calendar.rb +8 -0
- data/lib/openc3/script/script.rb +19 -0
- data/lib/openc3/script/storage.rb +6 -6
- data/lib/openc3/system/system.rb +6 -6
- data/lib/openc3/tools/cmd_tlm_server/interface_thread.rb +0 -2
- data/lib/openc3/top_level.rb +15 -63
- data/lib/openc3/topics/limits_event_topic.rb +1 -1
- data/lib/openc3/utilities/bucket_utilities.rb +3 -1
- data/lib/openc3/utilities/cli_generator.rb +7 -0
- data/lib/openc3/utilities/cmd_log.rb +1 -1
- data/lib/openc3/utilities/local_mode.rb +3 -0
- data/lib/openc3/utilities/process_manager.rb +1 -1
- data/lib/openc3/utilities/python_proxy.rb +11 -4
- data/lib/openc3/utilities/questdb_client.rb +735 -19
- data/lib/openc3/utilities/running_script.rb +25 -7
- data/lib/openc3/utilities/script.rb +452 -0
- data/lib/openc3/utilities/secrets.rb +1 -1
- data/lib/openc3/version.rb +5 -5
- data/templates/conversion/conversion.py +0 -8
- data/templates/conversion/conversion.rb +0 -11
- 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 +16 -2
- data/lib/openc3/migrations/20251022000000_remove_unique_id.rb +0 -23
|
@@ -15,11 +15,10 @@
|
|
|
15
15
|
# This file may also be used under the terms of a commercial license
|
|
16
16
|
# if purchased from OpenC3, Inc.
|
|
17
17
|
|
|
18
|
-
require 'set'
|
|
19
18
|
require 'openc3/utilities/store'
|
|
20
19
|
require 'openc3/utilities/store_queued'
|
|
21
20
|
require 'openc3/utilities/questdb_client'
|
|
22
|
-
require 'openc3/models/target_model'
|
|
21
|
+
# require 'openc3/models/target_model' # Circular require
|
|
23
22
|
|
|
24
23
|
module OpenC3
|
|
25
24
|
class CvtModel
|
|
@@ -126,251 +125,7 @@ module OpenC3
|
|
|
126
125
|
end
|
|
127
126
|
|
|
128
127
|
def self.tsdb_lookup(items, start_time:, end_time: nil, scope: $openc3_scope)
|
|
129
|
-
|
|
130
|
-
names = []
|
|
131
|
-
nil_count = 0
|
|
132
|
-
# Cache packet definitions to avoid repeated lookups
|
|
133
|
-
packet_cache = {}
|
|
134
|
-
# Map column names to item type info for decoding
|
|
135
|
-
item_types = {}
|
|
136
|
-
# Track calculated timestamp items: { position => { source:, format:, table_index: } }
|
|
137
|
-
calculated_items = {}
|
|
138
|
-
# Track which timestamp columns we need per table
|
|
139
|
-
needed_timestamps = {} # { table_index => Set of column names }
|
|
140
|
-
current_position = 0
|
|
141
|
-
|
|
142
|
-
# Stored timestamp items that need conversion from timestamp_ns to float seconds
|
|
143
|
-
stored_timestamp_items = Set.new(['PACKET_TIMESECONDS', 'RECEIVED_TIMESECONDS'])
|
|
144
|
-
# Track stored timestamp items: { position => { column:, table_index: } }
|
|
145
|
-
stored_timestamp_positions = {}
|
|
146
|
-
|
|
147
|
-
items.each do |item|
|
|
148
|
-
target_name, packet_name, orig_item_name, value_type, limits = item
|
|
149
|
-
# They will all be nil when item is a nil value
|
|
150
|
-
# A nil value indicates a value that does not exist as returned by get_tlm_available
|
|
151
|
-
if orig_item_name.nil?
|
|
152
|
-
# We know PACKET_TIMESECONDS always exists so we can use it to fill in the nil value
|
|
153
|
-
names << "PACKET_TIMESECONDS as __nil#{nil_count}"
|
|
154
|
-
nil_count += 1
|
|
155
|
-
current_position += 1
|
|
156
|
-
next
|
|
157
|
-
end
|
|
158
|
-
table_name = QuestDBClient.sanitize_table_name(target_name, packet_name, scope: scope)
|
|
159
|
-
tables[table_name] = 1
|
|
160
|
-
index = tables.find_index {|k,v| k == table_name }
|
|
161
|
-
|
|
162
|
-
# Check if this is a stored timestamp item (PACKET_TIMESECONDS or RECEIVED_TIMESECONDS)
|
|
163
|
-
# These are stored as timestamp_ns columns and need conversion to float seconds on read
|
|
164
|
-
if stored_timestamp_items.include?(orig_item_name)
|
|
165
|
-
col_name = "T#{index}.#{orig_item_name}"
|
|
166
|
-
names << "\"#{col_name}\""
|
|
167
|
-
stored_timestamp_positions[current_position] = { column: col_name, table_index: index }
|
|
168
|
-
current_position += 1
|
|
169
|
-
next
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
# Check if this is a calculated timestamp item (PACKET_TIMEFORMATTED or RECEIVED_TIMEFORMATTED)
|
|
173
|
-
if QuestDBClient::TIMESTAMP_ITEMS.key?(orig_item_name)
|
|
174
|
-
ts_info = QuestDBClient::TIMESTAMP_ITEMS[orig_item_name]
|
|
175
|
-
calculated_items[current_position] = {
|
|
176
|
-
source: ts_info[:source],
|
|
177
|
-
format: ts_info[:format],
|
|
178
|
-
table_index: index
|
|
179
|
-
}
|
|
180
|
-
# Track that we need this timestamp column for this table
|
|
181
|
-
needed_timestamps[index] ||= Set.new
|
|
182
|
-
needed_timestamps[index] << ts_info[:source]
|
|
183
|
-
current_position += 1
|
|
184
|
-
next
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
safe_item_name = QuestDBClient.sanitize_column_name(orig_item_name)
|
|
188
|
-
|
|
189
|
-
# Look up item type info from packet definition
|
|
190
|
-
cache_key = [target_name, packet_name]
|
|
191
|
-
unless packet_cache.key?(cache_key)
|
|
192
|
-
begin
|
|
193
|
-
packet_cache[cache_key] = TargetModel.packet(target_name, packet_name, scope: scope)
|
|
194
|
-
rescue RuntimeError
|
|
195
|
-
packet_cache[cache_key] = nil
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
packet_def = packet_cache[cache_key]
|
|
200
|
-
item_def = nil
|
|
201
|
-
if packet_def
|
|
202
|
-
packet_def['items']&.each do |pkt_item|
|
|
203
|
-
if pkt_item['name'] == orig_item_name
|
|
204
|
-
item_def = pkt_item
|
|
205
|
-
break
|
|
206
|
-
end
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
case value_type
|
|
211
|
-
when 'FORMATTED', 'WITH_UNITS'
|
|
212
|
-
col_name = "T#{index}.#{safe_item_name}__F"
|
|
213
|
-
names << "\"#{col_name}\""
|
|
214
|
-
# Formatted values are always strings, no special decoding needed
|
|
215
|
-
item_types[col_name] = { 'data_type' => 'STRING', 'array_size' => nil }
|
|
216
|
-
when 'CONVERTED'
|
|
217
|
-
col_name = "T#{index}.#{safe_item_name}__C"
|
|
218
|
-
names << "\"#{col_name}\""
|
|
219
|
-
# Converted values may have different types based on read_conversion
|
|
220
|
-
if item_def
|
|
221
|
-
rc = item_def['read_conversion']
|
|
222
|
-
if rc && rc['converted_type']
|
|
223
|
-
item_types[col_name] = { 'data_type' => rc['converted_type'], 'array_size' => item_def['array_size'] }
|
|
224
|
-
elsif item_def['states']
|
|
225
|
-
# State values are strings
|
|
226
|
-
item_types[col_name] = { 'data_type' => 'STRING', 'array_size' => nil }
|
|
227
|
-
else
|
|
228
|
-
item_types[col_name] = { 'data_type' => item_def['data_type'], 'array_size' => item_def['array_size'] }
|
|
229
|
-
end
|
|
230
|
-
else
|
|
231
|
-
item_types[col_name] = { 'data_type' => nil, 'array_size' => nil }
|
|
232
|
-
end
|
|
233
|
-
else
|
|
234
|
-
col_name = "T#{index}.#{safe_item_name}"
|
|
235
|
-
names << "\"#{col_name}\""
|
|
236
|
-
if item_def
|
|
237
|
-
item_types[col_name] = { 'data_type' => item_def['data_type'], 'array_size' => item_def['array_size'] }
|
|
238
|
-
else
|
|
239
|
-
item_types[col_name] = { 'data_type' => nil, 'array_size' => nil }
|
|
240
|
-
end
|
|
241
|
-
end
|
|
242
|
-
current_position += 1
|
|
243
|
-
if limits
|
|
244
|
-
names << "\"T#{index}.#{safe_item_name}__L\""
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
# Add needed timestamp columns to the SELECT
|
|
249
|
-
# Track which column alias maps to which timestamp source for result processing
|
|
250
|
-
# Note: We use underscores in the alias name to avoid needing quotes, which QuestDB includes in returned field names
|
|
251
|
-
timestamp_columns = {} # { "T0___ts_timestamp" => { table_index: 0, source: 'timestamp' } }
|
|
252
|
-
needed_timestamps.each do |table_index, ts_columns|
|
|
253
|
-
ts_columns.each do |ts_col|
|
|
254
|
-
alias_name = "T#{table_index}___ts_#{ts_col}"
|
|
255
|
-
names << "T#{table_index}.#{ts_col} as #{alias_name}"
|
|
256
|
-
timestamp_columns[alias_name] = { table_index: table_index, source: ts_col }
|
|
257
|
-
end
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
# Build the SQL query
|
|
261
|
-
query = "SELECT #{names.join(", ")} FROM "
|
|
262
|
-
tables.each_with_index do |(table_name, _), index|
|
|
263
|
-
if index == 0
|
|
264
|
-
query += "#{table_name} as T#{index} "
|
|
265
|
-
else
|
|
266
|
-
query += "ASOF JOIN #{table_name} as T#{index} "
|
|
267
|
-
end
|
|
268
|
-
end
|
|
269
|
-
query_params = []
|
|
270
|
-
if start_time && !end_time
|
|
271
|
-
query += "WHERE T0.PACKET_TIMESECONDS < $1 LIMIT -1"
|
|
272
|
-
query_params << start_time
|
|
273
|
-
elsif start_time && end_time
|
|
274
|
-
query += "WHERE T0.PACKET_TIMESECONDS >= $1 AND T0.PACKET_TIMESECONDS < $2"
|
|
275
|
-
query_params << start_time
|
|
276
|
-
query_params << end_time
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
retry_count = 0
|
|
280
|
-
begin
|
|
281
|
-
conn = QuestDBClient.connection
|
|
282
|
-
result = conn.exec_params(query, query_params)
|
|
283
|
-
if result.nil? or result.ntuples == 0
|
|
284
|
-
return {}
|
|
285
|
-
else
|
|
286
|
-
data = []
|
|
287
|
-
# Build up a results set that is an array of arrays
|
|
288
|
-
# Each nested array is a set of 2 items: [value, limits state]
|
|
289
|
-
# If the item does not have limits the limits state is nil
|
|
290
|
-
result.each_with_index do |tuples, row_num|
|
|
291
|
-
data[row_num] ||= []
|
|
292
|
-
row_index = 0
|
|
293
|
-
# Store timestamp values for this row: { "T0.PACKET_TIMESECONDS" => Time, ... }
|
|
294
|
-
row_timestamps = {}
|
|
295
|
-
tuples.each do |tuple|
|
|
296
|
-
col_name = tuple[0]
|
|
297
|
-
col_value = tuple[1]
|
|
298
|
-
if col_name.include?("__L")
|
|
299
|
-
data[row_num][row_index - 1][1] = col_value
|
|
300
|
-
elsif col_name =~ /^__nil/
|
|
301
|
-
data[row_num][row_index] = [nil, nil]
|
|
302
|
-
row_index += 1
|
|
303
|
-
elsif col_name =~ /^T(\d+)___ts_(.+)$/
|
|
304
|
-
# This is a timestamp column for calculated items (TIMEFORMATTED)
|
|
305
|
-
table_idx = $1.to_i
|
|
306
|
-
ts_source = $2
|
|
307
|
-
row_timestamps["T#{table_idx}.#{ts_source}"] = col_value
|
|
308
|
-
elsif col_name.end_with?('.PACKET_TIMESECONDS', '.RECEIVED_TIMESECONDS') || col_name == 'PACKET_TIMESECONDS' || col_name == 'RECEIVED_TIMESECONDS'
|
|
309
|
-
# Stored timestamp column - convert from datetime to float seconds
|
|
310
|
-
ts_utc = QuestDBClient.pg_timestamp_to_utc(col_value)
|
|
311
|
-
seconds_value = QuestDBClient.format_timestamp(ts_utc, :seconds)
|
|
312
|
-
data[row_num][row_index] = [seconds_value, nil]
|
|
313
|
-
row_index += 1
|
|
314
|
-
# Also store for calculated items (TIMEFORMATTED) that may need this
|
|
315
|
-
# Normalize key to T{index}.{col} format for consistency
|
|
316
|
-
if col_name.include?('.')
|
|
317
|
-
row_timestamps[col_name] = col_value
|
|
318
|
-
else
|
|
319
|
-
row_timestamps["T0.#{col_name}"] = col_value
|
|
320
|
-
end
|
|
321
|
-
else
|
|
322
|
-
# Decode value using item type info
|
|
323
|
-
# QuestDB may return column names without table alias prefix
|
|
324
|
-
# Try both the raw column name and prefixed versions
|
|
325
|
-
type_info = item_types[col_name]
|
|
326
|
-
unless type_info
|
|
327
|
-
tables.length.times do |i|
|
|
328
|
-
prefixed_name = "T#{i}.#{col_name}"
|
|
329
|
-
type_info = item_types[prefixed_name]
|
|
330
|
-
break if type_info
|
|
331
|
-
end
|
|
332
|
-
type_info ||= {}
|
|
333
|
-
end
|
|
334
|
-
decoded_value = QuestDBClient.decode_value(
|
|
335
|
-
col_value,
|
|
336
|
-
data_type: type_info['data_type'],
|
|
337
|
-
array_size: type_info['array_size']
|
|
338
|
-
)
|
|
339
|
-
data[row_num][row_index] = [decoded_value, nil]
|
|
340
|
-
row_index += 1
|
|
341
|
-
end
|
|
342
|
-
end
|
|
343
|
-
|
|
344
|
-
# Insert calculated timestamp items at their positions
|
|
345
|
-
# Insert in ascending order so positions remain valid after each insert
|
|
346
|
-
calculated_items.keys.sort.each do |position|
|
|
347
|
-
calc_info = calculated_items[position]
|
|
348
|
-
ts_key = "T#{calc_info[:table_index]}.#{calc_info[:source]}"
|
|
349
|
-
ts_value = row_timestamps[ts_key]
|
|
350
|
-
ts_utc = QuestDBClient.pg_timestamp_to_utc(ts_value)
|
|
351
|
-
calculated_value = QuestDBClient.format_timestamp(ts_utc, calc_info[:format])
|
|
352
|
-
data[row_num].insert(position, [calculated_value, nil])
|
|
353
|
-
end
|
|
354
|
-
end
|
|
355
|
-
# If we only have one row then we return a single array
|
|
356
|
-
if result.ntuples == 1
|
|
357
|
-
data = data[0]
|
|
358
|
-
end
|
|
359
|
-
return data
|
|
360
|
-
end
|
|
361
|
-
rescue IOError, PG::Error => e
|
|
362
|
-
# Retry the query because various errors can occur that are recoverable
|
|
363
|
-
retry_count += 1
|
|
364
|
-
if retry_count > 4
|
|
365
|
-
# After the 5th retry just raise the error
|
|
366
|
-
raise "Error querying TSDB: #{e.message}"
|
|
367
|
-
end
|
|
368
|
-
Logger.warn("TSDB: Retrying due to error: #{e.message}")
|
|
369
|
-
Logger.warn("TSDB: Last query: #{query}") # Log the last query for debugging
|
|
370
|
-
QuestDBClient.disconnect
|
|
371
|
-
sleep 0.1
|
|
372
|
-
retry
|
|
373
|
-
end
|
|
128
|
+
QuestDBClient.tsdb_lookup(items, start_time: start_time, end_time: end_time, scope: scope)
|
|
374
129
|
end
|
|
375
130
|
|
|
376
131
|
# Return all item values and limit state from the CVT
|
|
@@ -18,7 +18,7 @@ module OpenC3
|
|
|
18
18
|
class PluginStoreModel < Model
|
|
19
19
|
PRIMARY_KEY = 'openc3_plugin_store'
|
|
20
20
|
DEFAULT_STORE_URL = 'https://store.openc3.com'
|
|
21
|
-
JSON_ENDPOINT = '/api/v1.
|
|
21
|
+
JSON_ENDPOINT = '/api/v1.2/cosmos_plugins'
|
|
22
22
|
|
|
23
23
|
def self.set(plugin_store_data)
|
|
24
24
|
Store.set(PRIMARY_KEY, plugin_store_data)
|
|
@@ -164,36 +164,41 @@ module OpenC3
|
|
|
164
164
|
if target_name.include?('..') || target_name.include?('/') || target_name.include?('\\')
|
|
165
165
|
raise ArgumentError, "Invalid target_name: #{target_name.inspect}"
|
|
166
166
|
end
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
temp_dir = Dir.mktmpdir
|
|
168
|
+
begin
|
|
169
|
+
zip_filename = OpenC3.sanitize_path(File.join(temp_dir, "#{target_name}.zip"))
|
|
170
|
+
Zip.continue_on_exists_proc = true
|
|
171
|
+
zip = Zip::File.open(zip_filename, create: true)
|
|
171
172
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
173
|
+
if ENV['OPENC3_LOCAL_MODE']
|
|
174
|
+
OpenC3::LocalMode.zip_target(target_name, zip, scope: scope)
|
|
175
|
+
else
|
|
176
|
+
bucket = Bucket.getClient()
|
|
177
|
+
# The trailing slash is important!
|
|
178
|
+
prefix = "#{scope}/targets_modified/#{target_name}/"
|
|
179
|
+
resp = bucket.list_objects(
|
|
180
|
+
bucket: ENV['OPENC3_CONFIG_BUCKET'],
|
|
181
|
+
prefix: prefix,
|
|
182
|
+
)
|
|
183
|
+
resp.each do |item|
|
|
184
|
+
# item.key looks like DEFAULT/targets_modified/INST/screens/blah.txt
|
|
185
|
+
base_path = item.key.sub(prefix, '') # remove prefix
|
|
186
|
+
local_path = File.join(temp_dir, base_path)
|
|
187
|
+
# Ensure dir structure exists, get_object fails if not
|
|
188
|
+
FileUtils.mkdir_p(File.dirname(local_path))
|
|
189
|
+
bucket.get_object(bucket: ENV['OPENC3_CONFIG_BUCKET'], key: item.key, path: local_path)
|
|
190
|
+
zip.add(base_path, local_path)
|
|
191
|
+
end
|
|
190
192
|
end
|
|
193
|
+
zip.close
|
|
194
|
+
|
|
195
|
+
result = OpenStruct.new
|
|
196
|
+
result.filename = File.basename(zip_filename)
|
|
197
|
+
result.contents = File.read(zip_filename, mode: 'rb')
|
|
198
|
+
ensure
|
|
199
|
+
FileUtils.remove_entry_secure(temp_dir, true)
|
|
191
200
|
end
|
|
192
|
-
zip.close
|
|
193
201
|
|
|
194
|
-
result = OpenStruct.new
|
|
195
|
-
result.filename = File.basename(zip_filename)
|
|
196
|
-
result.contents = File.read(zip_filename, mode: 'rb')
|
|
197
202
|
return result
|
|
198
203
|
end
|
|
199
204
|
|
|
@@ -393,14 +398,7 @@ module OpenC3
|
|
|
393
398
|
shard: 0,
|
|
394
399
|
scope:
|
|
395
400
|
)
|
|
396
|
-
super("#{scope}__#{PRIMARY_KEY}", name: name, plugin: plugin, updated_at: updated_at,
|
|
397
|
-
cmd_buffer_depth: cmd_buffer_depth, cmd_log_cycle_time: cmd_log_cycle_time, cmd_log_cycle_size: cmd_log_cycle_size,
|
|
398
|
-
cmd_log_retain_time: cmd_log_retain_time,
|
|
399
|
-
tlm_buffer_depth: tlm_buffer_depth, tlm_log_cycle_time: tlm_log_cycle_time, tlm_log_cycle_size: tlm_log_cycle_size,
|
|
400
|
-
tlm_log_retain_time: tlm_log_retain_time,
|
|
401
|
-
cmd_decom_retain_time: cmd_decom_retain_time, tlm_decom_retain_time: tlm_decom_retain_time,
|
|
402
|
-
cleanup_poll_time: cleanup_poll_time, needs_dependencies: needs_dependencies, target_microservices: target_microservices,
|
|
403
|
-
scope: scope)
|
|
401
|
+
super("#{scope}__#{PRIMARY_KEY}", name: name, plugin: plugin, updated_at: updated_at, scope: scope)
|
|
404
402
|
@folder_name = folder_name
|
|
405
403
|
@requires = requires
|
|
406
404
|
@ignored_parameters = ignored_parameters
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
# if purchased from OpenC3, Inc.
|
|
17
17
|
|
|
18
18
|
require 'openc3/models/model'
|
|
19
|
-
require 'openc3/models/scope_model'
|
|
19
|
+
# require 'openc3/models/scope_model' # Circular require
|
|
20
20
|
require 'openc3/utilities/bucket'
|
|
21
21
|
require 'openc3/utilities/bucket_utilities'
|
|
22
22
|
require 'rack'
|
|
@@ -179,7 +179,7 @@ module OpenC3
|
|
|
179
179
|
end
|
|
180
180
|
end
|
|
181
181
|
|
|
182
|
-
if @url and !@url.start_with?('/') and @url !~ URI::
|
|
182
|
+
if @url and !@url.start_with?('/') and @url !~ URI::RFC2396_PARSER.make_regexp
|
|
183
183
|
raise "URL must be a full URL (http://domain.com/path) or a relative path (/path)"
|
|
184
184
|
end
|
|
185
185
|
|
|
@@ -250,9 +250,22 @@ module OpenC3
|
|
|
250
250
|
|
|
251
251
|
variables["tool_name"] = @name
|
|
252
252
|
start_path = "/tools/#{@folder_name}/"
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
253
|
+
# Sort files so dependencies are uploaded before dependents:
|
|
254
|
+
# fonts first, then CSS, then index.html last (it triggers all other loads)
|
|
255
|
+
filenames = Dir.glob(gem_path + start_path + "**/*")
|
|
256
|
+
filenames.reject! { |f| f == '.' or f == '..' or File.directory?(f) }
|
|
257
|
+
filenames.sort_by! do |filename|
|
|
258
|
+
if filename.include?('/fonts/')
|
|
259
|
+
[0, filename]
|
|
260
|
+
elsif filename.include?('/css/')
|
|
261
|
+
[1, filename]
|
|
262
|
+
elsif File.basename(filename) == 'index.html'
|
|
263
|
+
[3, filename]
|
|
264
|
+
else
|
|
265
|
+
[2, filename]
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
filenames.each do |filename|
|
|
256
269
|
key = filename.split(gem_path + '/tools/')[-1]
|
|
257
270
|
extension = filename.split('.')[-1]
|
|
258
271
|
content_type = Rack::Mime.mime_type(".#{extension}")
|
|
@@ -19,7 +19,7 @@ require 'openc3/models/model'
|
|
|
19
19
|
require 'openc3/models/microservice_model'
|
|
20
20
|
require 'openc3/models/target_model'
|
|
21
21
|
require 'openc3/models/trigger_group_model'
|
|
22
|
-
require 'openc3/models/reaction_model'
|
|
22
|
+
# require 'openc3/models/reaction_model' # Remove circular require
|
|
23
23
|
require 'openc3/topics/autonomic_topic'
|
|
24
24
|
|
|
25
25
|
module OpenC3
|
|
@@ -15,9 +15,8 @@
|
|
|
15
15
|
# This file may also be used under the terms of a commercial license
|
|
16
16
|
# if purchased from OpenC3, Inc.
|
|
17
17
|
|
|
18
|
-
require 'openc3/top_level'
|
|
19
18
|
require 'openc3/models/model'
|
|
20
|
-
require 'openc3/models/scope_model'
|
|
19
|
+
# require 'openc3/models/scope_model' # Circular require
|
|
21
20
|
require 'openc3/utilities/bucket'
|
|
22
21
|
require 'openc3/utilities/bucket_utilities'
|
|
23
22
|
|
|
@@ -54,8 +54,10 @@ module OpenC3
|
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
def finalize
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
unless closed?
|
|
58
|
+
extract()
|
|
59
|
+
close()
|
|
60
|
+
end
|
|
59
61
|
unlink()
|
|
60
62
|
|
|
61
63
|
output = ''
|
|
@@ -174,7 +176,7 @@ module OpenC3
|
|
|
174
176
|
end
|
|
175
177
|
@process.stop
|
|
176
178
|
end
|
|
177
|
-
FileUtils.remove_entry_secure(@temp_dir, true)
|
|
179
|
+
FileUtils.remove_entry_secure(@temp_dir, true) if @temp_dir
|
|
178
180
|
@process = nil
|
|
179
181
|
end
|
|
180
182
|
|
|
@@ -300,6 +302,7 @@ module OpenC3
|
|
|
300
302
|
# Respawn process
|
|
301
303
|
output = p.extract_output
|
|
302
304
|
Logger.error("Unexpected process died... respawning! #{p.cmd_line}\n#{output}\n", scope: p.scope)
|
|
305
|
+
p.hard_stop
|
|
303
306
|
p.start
|
|
304
307
|
end
|
|
305
308
|
end
|
|
@@ -308,6 +311,7 @@ module OpenC3
|
|
|
308
311
|
|
|
309
312
|
def shutdown_processes(processes)
|
|
310
313
|
# Make a copy so we don't mutate original
|
|
314
|
+
hard_stop_processes = processes.dup
|
|
311
315
|
processes = processes.dup
|
|
312
316
|
|
|
313
317
|
Logger.info("Commanding soft stops...")
|
|
@@ -331,10 +335,8 @@ module OpenC3
|
|
|
331
335
|
end
|
|
332
336
|
sleep(0.1)
|
|
333
337
|
end
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
processes.each { |_name, p| p.hard_stop }
|
|
337
|
-
end
|
|
338
|
+
Logger.debug("Commanding hard stops...")
|
|
339
|
+
hard_stop_processes.each { |_name, p| p.output_increment; p.extract_output; p.hard_stop }
|
|
338
340
|
end
|
|
339
341
|
|
|
340
342
|
def shutdown
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
# if purchased from OpenC3, Inc.
|
|
17
17
|
|
|
18
18
|
require 'digest'
|
|
19
|
+
require 'active_support/core_ext/object/deep_dup'
|
|
19
20
|
require 'openc3/packets/structure'
|
|
20
21
|
require 'openc3/packets/packet_item'
|
|
21
22
|
require 'openc3/ext/packet' if RUBY_ENGINE == 'ruby' and !ENV['OPENC3_NO_EXT']
|
|
@@ -252,7 +252,7 @@ module OpenC3
|
|
|
252
252
|
#######################################################################
|
|
253
253
|
when 'STATE', 'READ_CONVERSION', 'WRITE_CONVERSION', 'POLY_READ_CONVERSION',\
|
|
254
254
|
'POLY_WRITE_CONVERSION', 'SEG_POLY_READ_CONVERSION', 'SEG_POLY_WRITE_CONVERSION',\
|
|
255
|
-
'GENERIC_READ_CONVERSION_START', 'GENERIC_WRITE_CONVERSION_START', 'REQUIRED',\
|
|
255
|
+
'GENERIC_READ_CONVERSION_START', 'GENERIC_WRITE_CONVERSION_START', 'CONVERTED_DATA', 'REQUIRED',\
|
|
256
256
|
'LIMITS', 'LIMITS_RESPONSE', 'UNITS', 'FORMAT_STRING', 'DESCRIPTION',\
|
|
257
257
|
'MINIMUM_VALUE', 'MAXIMUM_VALUE', 'DEFAULT_VALUE', 'OVERFLOW', 'OVERLAP', 'KEY', 'VARIABLE_BIT_SIZE',\
|
|
258
258
|
'OBFUSCATE'
|
|
@@ -675,11 +675,6 @@ module OpenC3
|
|
|
675
675
|
klass = OpenC3.require_class(params[0])
|
|
676
676
|
conversion = klass.new(*params[1..(params.length - 1)])
|
|
677
677
|
@current_item.public_send("#{keyword.downcase}=".to_sym, conversion)
|
|
678
|
-
if klass != ProcessorConversion and (conversion.converted_type.nil? or conversion.converted_bit_size.nil?)
|
|
679
|
-
msg = "Read Conversion #{params[0]} on item #{@current_item.name} does not specify converted type or bit size"
|
|
680
|
-
@warnings << msg
|
|
681
|
-
Logger.instance.warn @warnings[-1]
|
|
682
|
-
end
|
|
683
678
|
else
|
|
684
679
|
conversion = PythonProxy.new('Conversion', params[0], *params[1..(params.length - 1)])
|
|
685
680
|
@current_item.public_send("#{keyword.downcase}=".to_sym, conversion)
|
|
@@ -719,8 +714,9 @@ module OpenC3
|
|
|
719
714
|
# All config.lines following this config.line are considered part
|
|
720
715
|
# of the conversion until an end of conversion marker is found
|
|
721
716
|
when 'GENERIC_READ_CONVERSION_START', 'GENERIC_WRITE_CONVERSION_START'
|
|
722
|
-
|
|
723
|
-
|
|
717
|
+
# As of COSMOS 7 the converted type and bit size are deprecated
|
|
718
|
+
# but we're still allowing them to be defined as parameters for backward compatibility
|
|
719
|
+
parser.verify_num_parameters(0, 2, keyword)
|
|
724
720
|
@proc_text = ''
|
|
725
721
|
@building_generic_conversion = true
|
|
726
722
|
parser.set_preserve_lines(true)
|
|
@@ -731,10 +727,30 @@ module OpenC3
|
|
|
731
727
|
raise parser.error("Invalid converted_type: #{@converted_type}.") unless CONVERTED_DATA_TYPES.include? @converted_type
|
|
732
728
|
end
|
|
733
729
|
@converted_bit_size = Integer(params[1]) if params[1]
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
730
|
+
|
|
731
|
+
# Define the converted data type, bit size, and optional array size
|
|
732
|
+
# for items with read conversions (especially DERIVED items)
|
|
733
|
+
when 'CONVERTED_DATA'
|
|
734
|
+
usage = "CONVERTED_DATA <Converted Bit Size> <Converted Type> <Converted Array Size (optional)>"
|
|
735
|
+
parser.verify_num_parameters(2, 3, usage)
|
|
736
|
+
raise parser.error("#{keyword} requires a current item") unless @current_item
|
|
737
|
+
raise parser.error("#{keyword} requires a current item with a conversion") unless @current_item.read_conversion or @current_item.write_conversion
|
|
738
|
+
converted_bit_size = Integer(params[0])
|
|
739
|
+
converted_type = params[1].upcase.intern
|
|
740
|
+
raise parser.error("Invalid converted_type: #{converted_type}.") unless CONVERTED_DATA_TYPES.include? converted_type
|
|
741
|
+
if @current_item.read_conversion
|
|
742
|
+
@current_item.read_conversion.converted_type = converted_type
|
|
743
|
+
@current_item.read_conversion.converted_bit_size = converted_bit_size
|
|
744
|
+
if params[2]
|
|
745
|
+
@current_item.read_conversion.converted_array_size = Integer(params[2])
|
|
746
|
+
end
|
|
747
|
+
end
|
|
748
|
+
if @current_item.write_conversion
|
|
749
|
+
@current_item.write_conversion.converted_type = converted_type
|
|
750
|
+
@current_item.write_conversion.converted_bit_size = converted_bit_size
|
|
751
|
+
if params[2]
|
|
752
|
+
@current_item.write_conversion.converted_array_size = Integer(params[2])
|
|
753
|
+
end
|
|
738
754
|
end
|
|
739
755
|
|
|
740
756
|
# Define a set of limits for the current telemetry item
|
|
@@ -52,6 +52,14 @@ module OpenC3
|
|
|
52
52
|
return _cal_handle_response(response, 'Failed to delete timeline')
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# Creates an activity for the specified timeline.
|
|
56
|
+
#
|
|
57
|
+
# @param name [String] The name of the timeline.
|
|
58
|
+
# @param kind [String] The kind of activity. Must be one of "COMMAND", "SCRIPT", or "RESERVE".
|
|
59
|
+
# @param start [DateTime] The start time of the activity.
|
|
60
|
+
# @param stop [DateTime] The stop time of the activity.
|
|
61
|
+
# @param data [Hash, optional] Additional data to associate with the activity. Defaults to {}. Any activity can provide "username", "notes", and "customTitle". "command", "script", and "reserve" keys are reserves for the corresponding activity kind, with "environment" also available for script activities.
|
|
62
|
+
# @param scope [String, optional] The scope of the activity. Defaults to OPENC3_SCOPE, must correspond to the timeline.
|
|
55
63
|
def create_timeline_activity(name, kind:, start:, stop:, data: {}, scope: $openc3_scope)
|
|
56
64
|
kind = kind.to_s.downcase()
|
|
57
65
|
kinds = %w(command script reserve)
|
data/lib/openc3/script/script.rb
CHANGED
|
@@ -47,6 +47,9 @@ $disconnect = false
|
|
|
47
47
|
$openc3_scope = ENV['OPENC3_SCOPE'] || 'DEFAULT'
|
|
48
48
|
$openc3_in_cluster = false
|
|
49
49
|
|
|
50
|
+
saved_verbose = $VERBOSE
|
|
51
|
+
$VERBOSE = false
|
|
52
|
+
|
|
50
53
|
module OpenC3
|
|
51
54
|
module Script
|
|
52
55
|
private
|
|
@@ -177,6 +180,10 @@ module OpenC3
|
|
|
177
180
|
message_box(string, *items, **options)
|
|
178
181
|
end
|
|
179
182
|
|
|
183
|
+
def check_box(string, *items, **options)
|
|
184
|
+
message_box(string, *items, **options)
|
|
185
|
+
end
|
|
186
|
+
|
|
180
187
|
def _file_dialog(title, message, filter:)
|
|
181
188
|
answer = ''
|
|
182
189
|
path = "./*"
|
|
@@ -199,6 +206,16 @@ module OpenC3
|
|
|
199
206
|
_file_dialog(title, message, filter)
|
|
200
207
|
end
|
|
201
208
|
|
|
209
|
+
def open_bucket_dialog(title, message = "Open Bucket File")
|
|
210
|
+
answer = ''
|
|
211
|
+
while answer.empty?
|
|
212
|
+
print "#{title}\n#{message}\n<Type bucket file path (e.g. BUCKET/path/to/file)>:"
|
|
213
|
+
answer = gets
|
|
214
|
+
answer.chomp!
|
|
215
|
+
end
|
|
216
|
+
return answer
|
|
217
|
+
end
|
|
218
|
+
|
|
202
219
|
def prompt(string, text_color: nil, background_color: nil, font_size: nil, font_family: nil, details: nil)
|
|
203
220
|
print "#{string}: "
|
|
204
221
|
print "Details: #{details}\n" if details
|
|
@@ -363,3 +380,5 @@ module OpenC3
|
|
|
363
380
|
end
|
|
364
381
|
end
|
|
365
382
|
end
|
|
383
|
+
|
|
384
|
+
$VERBOSE = saved_verbose
|