openc3 7.0.0.pre.rc2 → 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/bin/openc3cli +13 -4
- data/bin/pipinstall +6 -7
- data/bin/pipuninstall +3 -5
- data/data/config/interface_modifiers.yaml +1 -1
- data/data/config/item_modifiers.yaml +18 -6
- data/data/config/telemetry.yaml +1 -1
- data/data/config/widgets.yaml +10 -0
- data/lib/openc3/accessors/json_accessor.rb +1 -1
- data/lib/openc3/api/cmd_api.rb +2 -0
- data/lib/openc3/api/settings_api.rb +2 -0
- 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 +9 -2
- data/lib/openc3/microservices/scope_cleanup_microservice.rb +116 -1
- data/lib/openc3/microservices/text_log_microservice.rb +4 -1
- data/lib/openc3/migrations/20241208080000_no_critical_cmd.rb +1 -1
- data/lib/openc3/migrations/20250402000000_periodic_only_default.rb +1 -1
- data/lib/openc3/migrations/20260203000000_remove_store_id.rb +28 -0
- data/lib/openc3/migrations/20260204000000_remove_decom_reducer.rb +29 -1
- data/lib/openc3/models/activity_model.rb +41 -9
- data/lib/openc3/models/auth_model.rb +54 -19
- data/lib/openc3/models/cvt_model.rb +2 -265
- data/lib/openc3/models/model.rb +16 -0
- data/lib/openc3/models/plugin_model.rb +18 -12
- data/lib/openc3/models/plugin_store_model.rb +1 -1
- data/lib/openc3/models/python_package_model.rb +2 -2
- data/lib/openc3/models/queue_model.rb +5 -3
- data/lib/openc3/models/script_engine_model.rb +1 -1
- data/lib/openc3/models/target_model.rb +75 -42
- data/lib/openc3/models/tool_config_model.rb +12 -0
- 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 +2 -9
- 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/script/web_socket_api.rb +1 -1
- 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/command_topic.rb +1 -0
- data/lib/openc3/topics/limits_event_topic.rb +1 -1
- data/lib/openc3/utilities/authentication.rb +46 -7
- data/lib/openc3/utilities/authorization.rb +8 -1
- data/lib/openc3/utilities/aws_bucket.rb +2 -3
- 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 +764 -2
- 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 +19 -19
- data/lib/openc3/migrations/20251022000000_remove_unique_id.rb +0 -23
- data/lib/openc3/migrations/20251213120000_reinstall_plugins.rb +0 -45
|
@@ -15,20 +15,15 @@
|
|
|
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 'pg'
|
|
19
|
-
require 'set'
|
|
20
|
-
require 'thread'
|
|
21
18
|
require 'openc3/utilities/store'
|
|
22
19
|
require 'openc3/utilities/store_queued'
|
|
23
20
|
require 'openc3/utilities/questdb_client'
|
|
24
|
-
require 'openc3/models/target_model'
|
|
21
|
+
# require 'openc3/models/target_model' # Circular require
|
|
25
22
|
|
|
26
23
|
module OpenC3
|
|
27
24
|
class CvtModel
|
|
28
25
|
@@packet_cache = {}
|
|
29
26
|
@@override_cache = {}
|
|
30
|
-
@@conn = nil
|
|
31
|
-
@@conn_mutex = Mutex.new
|
|
32
27
|
|
|
33
28
|
VALUE_TYPES = [:RAW, :CONVERTED, :FORMATTED]
|
|
34
29
|
def self.build_json_from_packet(packet)
|
|
@@ -130,265 +125,7 @@ module OpenC3
|
|
|
130
125
|
end
|
|
131
126
|
|
|
132
127
|
def self.tsdb_lookup(items, start_time:, end_time: nil, scope: $openc3_scope)
|
|
133
|
-
|
|
134
|
-
names = []
|
|
135
|
-
nil_count = 0
|
|
136
|
-
# Cache packet definitions to avoid repeated lookups
|
|
137
|
-
packet_cache = {}
|
|
138
|
-
# Map column names to item type info for decoding
|
|
139
|
-
item_types = {}
|
|
140
|
-
# Track calculated timestamp items: { position => { source:, format:, table_index: } }
|
|
141
|
-
calculated_items = {}
|
|
142
|
-
# Track which timestamp columns we need per table
|
|
143
|
-
needed_timestamps = {} # { table_index => Set of column names }
|
|
144
|
-
current_position = 0
|
|
145
|
-
|
|
146
|
-
# Stored timestamp items that need conversion from timestamp_ns to float seconds
|
|
147
|
-
stored_timestamp_items = Set.new(['PACKET_TIMESECONDS', 'RECEIVED_TIMESECONDS'])
|
|
148
|
-
# Track stored timestamp items: { position => { column:, table_index: } }
|
|
149
|
-
stored_timestamp_positions = {}
|
|
150
|
-
|
|
151
|
-
items.each do |item|
|
|
152
|
-
target_name, packet_name, orig_item_name, value_type, limits = item
|
|
153
|
-
# They will all be nil when item is a nil value
|
|
154
|
-
# A nil value indicates a value that does not exist as returned by get_tlm_available
|
|
155
|
-
if orig_item_name.nil?
|
|
156
|
-
# We know PACKET_TIMESECONDS always exists so we can use it to fill in the nil value
|
|
157
|
-
names << "PACKET_TIMESECONDS as __nil#{nil_count}"
|
|
158
|
-
nil_count += 1
|
|
159
|
-
current_position += 1
|
|
160
|
-
next
|
|
161
|
-
end
|
|
162
|
-
table_name = QuestDBClient.sanitize_table_name(target_name, packet_name, scope: scope)
|
|
163
|
-
tables[table_name] = 1
|
|
164
|
-
index = tables.find_index {|k,v| k == table_name }
|
|
165
|
-
|
|
166
|
-
# Check if this is a stored timestamp item (PACKET_TIMESECONDS or RECEIVED_TIMESECONDS)
|
|
167
|
-
# These are stored as timestamp_ns columns and need conversion to float seconds on read
|
|
168
|
-
if stored_timestamp_items.include?(orig_item_name)
|
|
169
|
-
col_name = "T#{index}.#{orig_item_name}"
|
|
170
|
-
names << "\"#{col_name}\""
|
|
171
|
-
stored_timestamp_positions[current_position] = { column: col_name, table_index: index }
|
|
172
|
-
current_position += 1
|
|
173
|
-
next
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
# Check if this is a calculated timestamp item (PACKET_TIMEFORMATTED or RECEIVED_TIMEFORMATTED)
|
|
177
|
-
if QuestDBClient::TIMESTAMP_ITEMS.key?(orig_item_name)
|
|
178
|
-
ts_info = QuestDBClient::TIMESTAMP_ITEMS[orig_item_name]
|
|
179
|
-
calculated_items[current_position] = {
|
|
180
|
-
source: ts_info[:source],
|
|
181
|
-
format: ts_info[:format],
|
|
182
|
-
table_index: index
|
|
183
|
-
}
|
|
184
|
-
# Track that we need this timestamp column for this table
|
|
185
|
-
needed_timestamps[index] ||= Set.new
|
|
186
|
-
needed_timestamps[index] << ts_info[:source]
|
|
187
|
-
current_position += 1
|
|
188
|
-
next
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
safe_item_name = QuestDBClient.sanitize_column_name(orig_item_name)
|
|
192
|
-
|
|
193
|
-
# Look up item type info from packet definition
|
|
194
|
-
cache_key = [target_name, packet_name]
|
|
195
|
-
unless packet_cache.key?(cache_key)
|
|
196
|
-
begin
|
|
197
|
-
packet_cache[cache_key] = TargetModel.packet(target_name, packet_name, scope: scope)
|
|
198
|
-
rescue RuntimeError
|
|
199
|
-
packet_cache[cache_key] = nil
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
packet_def = packet_cache[cache_key]
|
|
204
|
-
item_def = nil
|
|
205
|
-
if packet_def
|
|
206
|
-
packet_def['items']&.each do |pkt_item|
|
|
207
|
-
if pkt_item['name'] == orig_item_name
|
|
208
|
-
item_def = pkt_item
|
|
209
|
-
break
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
case value_type
|
|
215
|
-
when 'FORMATTED', 'WITH_UNITS'
|
|
216
|
-
col_name = "T#{index}.#{safe_item_name}__F"
|
|
217
|
-
names << "\"#{col_name}\""
|
|
218
|
-
# Formatted values are always strings, no special decoding needed
|
|
219
|
-
item_types[col_name] = { 'data_type' => 'STRING', 'array_size' => nil }
|
|
220
|
-
when 'CONVERTED'
|
|
221
|
-
col_name = "T#{index}.#{safe_item_name}__C"
|
|
222
|
-
names << "\"#{col_name}\""
|
|
223
|
-
# Converted values may have different types based on read_conversion
|
|
224
|
-
if item_def
|
|
225
|
-
rc = item_def['read_conversion']
|
|
226
|
-
if rc && rc['converted_type']
|
|
227
|
-
item_types[col_name] = { 'data_type' => rc['converted_type'], 'array_size' => item_def['array_size'] }
|
|
228
|
-
elsif item_def['states']
|
|
229
|
-
# State values are strings
|
|
230
|
-
item_types[col_name] = { 'data_type' => 'STRING', 'array_size' => nil }
|
|
231
|
-
else
|
|
232
|
-
item_types[col_name] = { 'data_type' => item_def['data_type'], 'array_size' => item_def['array_size'] }
|
|
233
|
-
end
|
|
234
|
-
else
|
|
235
|
-
item_types[col_name] = { 'data_type' => nil, 'array_size' => nil }
|
|
236
|
-
end
|
|
237
|
-
else
|
|
238
|
-
col_name = "T#{index}.#{safe_item_name}"
|
|
239
|
-
names << "\"#{col_name}\""
|
|
240
|
-
if item_def
|
|
241
|
-
item_types[col_name] = { 'data_type' => item_def['data_type'], 'array_size' => item_def['array_size'] }
|
|
242
|
-
else
|
|
243
|
-
item_types[col_name] = { 'data_type' => nil, 'array_size' => nil }
|
|
244
|
-
end
|
|
245
|
-
end
|
|
246
|
-
current_position += 1
|
|
247
|
-
if limits
|
|
248
|
-
names << "\"T#{index}.#{safe_item_name}__L\""
|
|
249
|
-
end
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
# Add needed timestamp columns to the SELECT
|
|
253
|
-
# Track which column alias maps to which timestamp source for result processing
|
|
254
|
-
# Note: We use underscores in the alias name to avoid needing quotes, which QuestDB includes in returned field names
|
|
255
|
-
timestamp_columns = {} # { "T0___ts_timestamp" => { table_index: 0, source: 'timestamp' } }
|
|
256
|
-
needed_timestamps.each do |table_index, ts_columns|
|
|
257
|
-
ts_columns.each do |ts_col|
|
|
258
|
-
alias_name = "T#{table_index}___ts_#{ts_col}"
|
|
259
|
-
names << "T#{table_index}.#{ts_col} as #{alias_name}"
|
|
260
|
-
timestamp_columns[alias_name] = { table_index: table_index, source: ts_col }
|
|
261
|
-
end
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
# Build the SQL query
|
|
265
|
-
query = "SELECT #{names.join(", ")} FROM "
|
|
266
|
-
tables.each_with_index do |(table_name, _), index|
|
|
267
|
-
if index == 0
|
|
268
|
-
query += "#{table_name} as T#{index} "
|
|
269
|
-
else
|
|
270
|
-
query += "ASOF JOIN #{table_name} as T#{index} "
|
|
271
|
-
end
|
|
272
|
-
end
|
|
273
|
-
if start_time && !end_time
|
|
274
|
-
query += "WHERE T0.PACKET_TIMESECONDS < '#{start_time}' LIMIT -1"
|
|
275
|
-
elsif start_time && end_time
|
|
276
|
-
query += "WHERE T0.PACKET_TIMESECONDS >= '#{start_time}' AND T0.PACKET_TIMESECONDS < '#{end_time}'"
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
retry_count = 0
|
|
280
|
-
begin
|
|
281
|
-
@@conn_mutex.synchronize do
|
|
282
|
-
@@conn ||= PG::Connection.new(host: ENV['OPENC3_TSDB_HOSTNAME'],
|
|
283
|
-
port: ENV['OPENC3_TSDB_QUERY_PORT'],
|
|
284
|
-
user: ENV['OPENC3_TSDB_USERNAME'],
|
|
285
|
-
password: ENV['OPENC3_TSDB_PASSWORD'],
|
|
286
|
-
dbname: 'qdb')
|
|
287
|
-
# Default connection is all strings but we want to map to the correct types
|
|
288
|
-
if @@conn.type_map_for_results.is_a? PG::TypeMapAllStrings
|
|
289
|
-
# TODO: This doesn't seem to be round tripping UINT64 correctly
|
|
290
|
-
# Try playback with P_2.2,2 and P(:6;): from the DEMO
|
|
291
|
-
@@conn.type_map_for_results = PG::BasicTypeMapForResults.new @@conn
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
result = @@conn.exec(query)
|
|
295
|
-
if result.nil? or result.ntuples == 0
|
|
296
|
-
return {}
|
|
297
|
-
else
|
|
298
|
-
data = []
|
|
299
|
-
# Build up a results set that is an array of arrays
|
|
300
|
-
# Each nested array is a set of 2 items: [value, limits state]
|
|
301
|
-
# If the item does not have limits the limits state is nil
|
|
302
|
-
result.each_with_index do |tuples, row_num|
|
|
303
|
-
data[row_num] ||= []
|
|
304
|
-
row_index = 0
|
|
305
|
-
# Store timestamp values for this row: { "T0.PACKET_TIMESECONDS" => Time, ... }
|
|
306
|
-
row_timestamps = {}
|
|
307
|
-
tuples.each do |tuple|
|
|
308
|
-
col_name = tuple[0]
|
|
309
|
-
col_value = tuple[1]
|
|
310
|
-
if col_name.include?("__L")
|
|
311
|
-
data[row_num][row_index - 1][1] = col_value
|
|
312
|
-
elsif col_name =~ /^__nil/
|
|
313
|
-
data[row_num][row_index] = [nil, nil]
|
|
314
|
-
row_index += 1
|
|
315
|
-
elsif col_name =~ /^T(\d+)___ts_(.+)$/
|
|
316
|
-
# This is a timestamp column for calculated items (TIMEFORMATTED)
|
|
317
|
-
table_idx = $1.to_i
|
|
318
|
-
ts_source = $2
|
|
319
|
-
row_timestamps["T#{table_idx}.#{ts_source}"] = col_value
|
|
320
|
-
elsif col_name.end_with?('.PACKET_TIMESECONDS', '.RECEIVED_TIMESECONDS') || col_name == 'PACKET_TIMESECONDS' || col_name == 'RECEIVED_TIMESECONDS'
|
|
321
|
-
# Stored timestamp column - convert from datetime to float seconds
|
|
322
|
-
ts_utc = QuestDBClient.pg_timestamp_to_utc(col_value)
|
|
323
|
-
seconds_value = QuestDBClient.format_timestamp(ts_utc, :seconds)
|
|
324
|
-
data[row_num][row_index] = [seconds_value, nil]
|
|
325
|
-
row_index += 1
|
|
326
|
-
# Also store for calculated items (TIMEFORMATTED) that may need this
|
|
327
|
-
# Normalize key to T{index}.{col} format for consistency
|
|
328
|
-
if col_name.include?('.')
|
|
329
|
-
row_timestamps[col_name] = col_value
|
|
330
|
-
else
|
|
331
|
-
row_timestamps["T0.#{col_name}"] = col_value
|
|
332
|
-
end
|
|
333
|
-
else
|
|
334
|
-
# Decode value using item type info
|
|
335
|
-
# QuestDB may return column names without table alias prefix
|
|
336
|
-
# Try both the raw column name and prefixed versions
|
|
337
|
-
type_info = item_types[col_name]
|
|
338
|
-
unless type_info
|
|
339
|
-
tables.length.times do |i|
|
|
340
|
-
prefixed_name = "T#{i}.#{col_name}"
|
|
341
|
-
type_info = item_types[prefixed_name]
|
|
342
|
-
break if type_info
|
|
343
|
-
end
|
|
344
|
-
type_info ||= {}
|
|
345
|
-
end
|
|
346
|
-
decoded_value = QuestDBClient.decode_value(
|
|
347
|
-
col_value,
|
|
348
|
-
data_type: type_info['data_type'],
|
|
349
|
-
array_size: type_info['array_size']
|
|
350
|
-
)
|
|
351
|
-
data[row_num][row_index] = [decoded_value, nil]
|
|
352
|
-
row_index += 1
|
|
353
|
-
end
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
# Insert calculated timestamp items at their positions
|
|
357
|
-
# Insert in ascending order so positions remain valid after each insert
|
|
358
|
-
calculated_items.keys.sort.each do |position|
|
|
359
|
-
calc_info = calculated_items[position]
|
|
360
|
-
ts_key = "T#{calc_info[:table_index]}.#{calc_info[:source]}"
|
|
361
|
-
ts_value = row_timestamps[ts_key]
|
|
362
|
-
ts_utc = QuestDBClient.pg_timestamp_to_utc(ts_value)
|
|
363
|
-
calculated_value = QuestDBClient.format_timestamp(ts_utc, calc_info[:format])
|
|
364
|
-
data[row_num].insert(position, [calculated_value, nil])
|
|
365
|
-
end
|
|
366
|
-
end
|
|
367
|
-
# If we only have one row then we return a single array
|
|
368
|
-
if result.ntuples == 1
|
|
369
|
-
data = data[0]
|
|
370
|
-
end
|
|
371
|
-
return data
|
|
372
|
-
end
|
|
373
|
-
end
|
|
374
|
-
rescue IOError, PG::Error => e
|
|
375
|
-
# Retry the query because various errors can occur that are recoverable
|
|
376
|
-
retry_count += 1
|
|
377
|
-
if retry_count > 4
|
|
378
|
-
# After the 5th retry just raise the error
|
|
379
|
-
raise "Error querying TSDB: #{e.message}"
|
|
380
|
-
end
|
|
381
|
-
Logger.warn("TSDB: Retrying due to error: #{e.message}")
|
|
382
|
-
Logger.warn("TSDB: Last query: #{query}") # Log the last query for debugging
|
|
383
|
-
@@conn_mutex.synchronize do
|
|
384
|
-
if @@conn and !@@conn.finished?
|
|
385
|
-
@@conn.finish()
|
|
386
|
-
end
|
|
387
|
-
@@conn = nil # Force the new connection
|
|
388
|
-
end
|
|
389
|
-
sleep 0.1
|
|
390
|
-
retry
|
|
391
|
-
end
|
|
128
|
+
QuestDBClient.tsdb_lookup(items, start_time: start_time, end_time: end_time, scope: scope)
|
|
392
129
|
end
|
|
393
130
|
|
|
394
131
|
# Return all item values and limit state from the CVT
|
data/lib/openc3/models/model.rb
CHANGED
|
@@ -194,6 +194,22 @@ module OpenC3
|
|
|
194
194
|
'scope' => @scope }
|
|
195
195
|
end
|
|
196
196
|
|
|
197
|
+
# Compare this model's as_json with a previous Hash and return an array
|
|
198
|
+
# of human-readable change descriptions (e.g. "key: old -> new").
|
|
199
|
+
# Skips the updated_at field since it always changes.
|
|
200
|
+
# @param existing [Hash] the previous model state (from .get)
|
|
201
|
+
# @return [Array<String>] list of changed fields
|
|
202
|
+
def diff(existing)
|
|
203
|
+
changes = []
|
|
204
|
+
new_json = as_json
|
|
205
|
+
existing.each do |key, old_value|
|
|
206
|
+
next if key == 'updated_at'
|
|
207
|
+
new_value = new_json[key]
|
|
208
|
+
changes << "#{key}: #{old_value} -> #{new_value}" if old_value != new_value
|
|
209
|
+
end
|
|
210
|
+
changes
|
|
211
|
+
end
|
|
212
|
+
|
|
197
213
|
def check_disable_erb(filename)
|
|
198
214
|
erb_disabled = false
|
|
199
215
|
if @disable_erb
|
|
@@ -53,7 +53,8 @@ module OpenC3
|
|
|
53
53
|
attr_accessor :plugin_txt_lines
|
|
54
54
|
attr_accessor :minimum_cosmos_version
|
|
55
55
|
attr_accessor :needs_dependencies
|
|
56
|
-
attr_accessor :
|
|
56
|
+
attr_accessor :store_plugin_id
|
|
57
|
+
attr_accessor :store_version_id
|
|
57
58
|
attr_accessor :title
|
|
58
59
|
attr_accessor :description
|
|
59
60
|
attr_accessor :licenses
|
|
@@ -78,7 +79,7 @@ module OpenC3
|
|
|
78
79
|
|
|
79
80
|
# Called by the PluginsController to parse the plugin variables
|
|
80
81
|
# Doesn't actually create the plugin during the phase
|
|
81
|
-
def self.install_phase1(gem_file_path, existing_variables: nil, existing_plugin_txt_lines: nil,
|
|
82
|
+
def self.install_phase1(gem_file_path, existing_variables: nil, existing_plugin_txt_lines: nil, store_plugin_id: nil, store_version_id: nil, process_existing: false, scope:, validate_only: false)
|
|
82
83
|
gem_name = File.basename(gem_file_path).split("__")[0]
|
|
83
84
|
|
|
84
85
|
temp_dir = Dir.mktmpdir
|
|
@@ -167,8 +168,9 @@ module OpenC3
|
|
|
167
168
|
end
|
|
168
169
|
end
|
|
169
170
|
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
store_plugin_id = Integer(store_plugin_id) if store_plugin_id
|
|
172
|
+
store_version_id = Integer(store_version_id) if store_version_id
|
|
173
|
+
model = PluginModel.new(name: gem_name, variables: variables, plugin_txt_lines: plugin_txt_lines, store_plugin_id: store_plugin_id, store_version_id: store_version_id, minimum_cosmos_version: minimum_cosmos_version, scope: scope)
|
|
172
174
|
result = model.as_json()
|
|
173
175
|
result['existing_plugin_txt_lines'] = existing_plugin_txt_lines if existing_plugin_txt_lines and not process_existing and existing_plugin_txt_lines != result['plugin_txt_lines']
|
|
174
176
|
return result
|
|
@@ -233,7 +235,8 @@ module OpenC3
|
|
|
233
235
|
full_default_path = File.join(gem_path, default_img_path)
|
|
234
236
|
img_path = default_img_path if File.exist? full_default_path
|
|
235
237
|
end
|
|
236
|
-
|
|
238
|
+
package_name = "#{pkg.spec.name}-#{pkg.spec.version}"
|
|
239
|
+
plugin_model.img_path = File.join('gems', package_name, img_path) if img_path # convert this filesystem path to volumes mount path
|
|
237
240
|
plugin_model.update() unless validate_only
|
|
238
241
|
|
|
239
242
|
needs_dependencies = pkg.spec.runtime_dependencies.length > 0
|
|
@@ -265,16 +268,16 @@ module OpenC3
|
|
|
265
268
|
if File.exist?(pyproject_path)
|
|
266
269
|
Logger.info "Installing python packages from pyproject.toml with pypi_url=#{pypi_url}"
|
|
267
270
|
if ENV['PIP_ENABLE_TRUSTED_HOST'].nil?
|
|
268
|
-
pip_args = "
|
|
271
|
+
pip_args = "-i #{pypi_url} #{gem_path}"
|
|
269
272
|
else
|
|
270
|
-
pip_args = "
|
|
273
|
+
pip_args = "-i #{pypi_url} --trusted-host #{URI.parse(pypi_url).host} #{gem_path}"
|
|
271
274
|
end
|
|
272
275
|
else
|
|
273
276
|
Logger.info "Installing python packages from requirements.txt with pypi_url=#{pypi_url}"
|
|
274
277
|
if ENV['PIP_ENABLE_TRUSTED_HOST'].nil?
|
|
275
|
-
pip_args = "
|
|
278
|
+
pip_args = "-i #{pypi_url} -r #{requirements_path}"
|
|
276
279
|
else
|
|
277
|
-
pip_args = "
|
|
280
|
+
pip_args = "-i #{pypi_url} --trusted-host #{URI.parse(pypi_url).host} -r #{requirements_path}"
|
|
278
281
|
end
|
|
279
282
|
end
|
|
280
283
|
puts `/openc3/bin/pipinstall #{pip_args}`
|
|
@@ -377,7 +380,8 @@ module OpenC3
|
|
|
377
380
|
plugin_txt_lines: [],
|
|
378
381
|
minimum_cosmos_version: nil,
|
|
379
382
|
needs_dependencies: false,
|
|
380
|
-
|
|
383
|
+
store_plugin_id: nil,
|
|
384
|
+
store_version_id: nil,
|
|
381
385
|
title: nil,
|
|
382
386
|
description: nil,
|
|
383
387
|
keywords: nil,
|
|
@@ -393,7 +397,8 @@ module OpenC3
|
|
|
393
397
|
@plugin_txt_lines = plugin_txt_lines
|
|
394
398
|
@minimum_cosmos_version = minimum_cosmos_version
|
|
395
399
|
@needs_dependencies = ConfigParser.handle_true_false(needs_dependencies)
|
|
396
|
-
@
|
|
400
|
+
@store_plugin_id = store_plugin_id
|
|
401
|
+
@store_version_id = store_version_id
|
|
397
402
|
@title = title
|
|
398
403
|
@description = description
|
|
399
404
|
@keywords = keywords
|
|
@@ -420,7 +425,8 @@ module OpenC3
|
|
|
420
425
|
'plugin_txt_lines' => @plugin_txt_lines,
|
|
421
426
|
'minimum_cosmos_version' => @minimum_cosmos_version,
|
|
422
427
|
'needs_dependencies' => @needs_dependencies,
|
|
423
|
-
'
|
|
428
|
+
'store_plugin_id' => @store_plugin_id,
|
|
429
|
+
'store_version_id' => @store_version_id,
|
|
424
430
|
'title' => @title,
|
|
425
431
|
'description' => @description,
|
|
426
432
|
'keywords' => @keywords,
|
|
@@ -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)
|
|
@@ -88,9 +88,9 @@ module OpenC3
|
|
|
88
88
|
end
|
|
89
89
|
Logger.info "Installing python package: #{name_or_path}"
|
|
90
90
|
if ENV['PIP_ENABLE_TRUSTED_HOST'].nil?
|
|
91
|
-
pip_args = ["
|
|
91
|
+
pip_args = ["-i", pypi_url, package_file_path]
|
|
92
92
|
else
|
|
93
|
-
pip_args = ["
|
|
93
|
+
pip_args = ["-i", pypi_url, "--trusted-host", URI.parse(pypi_url).host, package_file_path]
|
|
94
94
|
end
|
|
95
95
|
result = OpenC3::ProcessManager.instance.spawn(["/openc3/bin/pipinstall"] + pip_args, "package_install", package_filename, Time.now + 3600.0, scope: scope)
|
|
96
96
|
return result.name
|
|
@@ -40,7 +40,7 @@ module OpenC3
|
|
|
40
40
|
end
|
|
41
41
|
# END NOTE
|
|
42
42
|
|
|
43
|
-
def self.queue_command(name, command: nil, target_name: nil, cmd_name: nil, cmd_params: nil, username:, scope:)
|
|
43
|
+
def self.queue_command(name, command: nil, target_name: nil, cmd_name: nil, cmd_params: nil, validate: true, timeout: nil, username:, scope:)
|
|
44
44
|
model = get_model(name: name, scope: scope)
|
|
45
45
|
raise QueueError, "Queue '#{name}' not found in scope '#{scope}'" unless model
|
|
46
46
|
|
|
@@ -53,7 +53,7 @@ module OpenC3
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
# Build command data with support for both formats
|
|
56
|
-
command_data = { username: username, timestamp: Time.now.to_nsec_from_epoch }
|
|
56
|
+
command_data = { username: username, timestamp: Time.now.to_nsec_from_epoch, validate: validate, timeout: timeout }
|
|
57
57
|
if target_name && cmd_name
|
|
58
58
|
# New format: store target_name, cmd_name, and cmd_params separately
|
|
59
59
|
command_data[:target_name] = target_name
|
|
@@ -142,7 +142,7 @@ module OpenC3
|
|
|
142
142
|
notify(kind: 'command')
|
|
143
143
|
end
|
|
144
144
|
|
|
145
|
-
def update_command(id:, command:, username:)
|
|
145
|
+
def update_command(id:, command:, username:, validate: nil, timeout: nil)
|
|
146
146
|
if @state == 'DISABLE'
|
|
147
147
|
raise QueueError, "Queue '#{@name}' is disabled. Command at id #{id} not updated."
|
|
148
148
|
end
|
|
@@ -156,6 +156,8 @@ module OpenC3
|
|
|
156
156
|
# Remove the existing command and add the new one at the same id
|
|
157
157
|
Store.zremrangebyscore("#{@scope}:#{@name}", id, id)
|
|
158
158
|
command_data = { username: username, value: command, timestamp: Time.now.to_nsec_from_epoch }
|
|
159
|
+
command_data[:validate] = validate unless validate.nil?
|
|
160
|
+
command_data[:timeout] = timeout unless timeout.nil?
|
|
159
161
|
Store.zadd("#{@scope}:#{@name}", id, command_data.to_json)
|
|
160
162
|
notify(kind: 'command')
|
|
161
163
|
end
|
|
@@ -33,6 +33,7 @@ require 'openc3/utilities/bucket'
|
|
|
33
33
|
require 'openc3/utilities/zip'
|
|
34
34
|
require 'fileutils'
|
|
35
35
|
require 'ostruct'
|
|
36
|
+
require 'set'
|
|
36
37
|
require 'tmpdir'
|
|
37
38
|
|
|
38
39
|
module OpenC3
|
|
@@ -55,6 +56,7 @@ module OpenC3
|
|
|
55
56
|
@@sync_packet_count_data = {}
|
|
56
57
|
@@sync_packet_count_time = nil
|
|
57
58
|
@@sync_packet_count_delay_seconds = 1.0 # Sync packet counts every second
|
|
59
|
+
@@stale_packet_keys_warned = Set.new # Track stale keys already warned about
|
|
58
60
|
|
|
59
61
|
attr_accessor :folder_name
|
|
60
62
|
attr_accessor :requires
|
|
@@ -162,36 +164,41 @@ module OpenC3
|
|
|
162
164
|
if target_name.include?('..') || target_name.include?('/') || target_name.include?('\\')
|
|
163
165
|
raise ArgumentError, "Invalid target_name: #{target_name.inspect}"
|
|
164
166
|
end
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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)
|
|
169
172
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
188
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)
|
|
189
200
|
end
|
|
190
|
-
zip.close
|
|
191
201
|
|
|
192
|
-
result = OpenStruct.new
|
|
193
|
-
result.filename = File.basename(zip_filename)
|
|
194
|
-
result.contents = File.read(zip_filename, mode: 'rb')
|
|
195
202
|
return result
|
|
196
203
|
end
|
|
197
204
|
|
|
@@ -391,14 +398,7 @@ module OpenC3
|
|
|
391
398
|
shard: 0,
|
|
392
399
|
scope:
|
|
393
400
|
)
|
|
394
|
-
super("#{scope}__#{PRIMARY_KEY}", name: name, plugin: plugin, updated_at: updated_at,
|
|
395
|
-
cmd_buffer_depth: cmd_buffer_depth, cmd_log_cycle_time: cmd_log_cycle_time, cmd_log_cycle_size: cmd_log_cycle_size,
|
|
396
|
-
cmd_log_retain_time: cmd_log_retain_time,
|
|
397
|
-
tlm_buffer_depth: tlm_buffer_depth, tlm_log_cycle_time: tlm_log_cycle_time, tlm_log_cycle_size: tlm_log_cycle_size,
|
|
398
|
-
tlm_log_retain_time: tlm_log_retain_time,
|
|
399
|
-
cmd_decom_retain_time: cmd_decom_retain_time, tlm_decom_retain_time: tlm_decom_retain_time,
|
|
400
|
-
cleanup_poll_time: cleanup_poll_time, needs_dependencies: needs_dependencies, target_microservices: target_microservices,
|
|
401
|
-
scope: scope)
|
|
401
|
+
super("#{scope}__#{PRIMARY_KEY}", name: name, plugin: plugin, updated_at: updated_at, scope: scope)
|
|
402
402
|
@folder_name = folder_name
|
|
403
403
|
@requires = requires
|
|
404
404
|
@ignored_parameters = ignored_parameters
|
|
@@ -1235,17 +1235,34 @@ module OpenC3
|
|
|
1235
1235
|
|
|
1236
1236
|
def self.init_tlm_packet_counts(tlm_target_names, scope:)
|
|
1237
1237
|
@@sync_packet_count_time = Time.now
|
|
1238
|
+
@@stale_packet_keys_warned = Set.new
|
|
1238
1239
|
|
|
1239
1240
|
# Get all the packet counts with the global counters
|
|
1240
1241
|
tlm_target_names.each do |target_name|
|
|
1241
1242
|
get_all_telemetry_counts(target_name, scope: scope).each do |packet_name, count|
|
|
1242
|
-
|
|
1243
|
-
|
|
1243
|
+
begin
|
|
1244
|
+
update_packet = System.telemetry.packet(target_name, packet_name)
|
|
1245
|
+
update_packet.received_count = count.to_i
|
|
1246
|
+
rescue RuntimeError
|
|
1247
|
+
key = "#{target_name} #{packet_name}"
|
|
1248
|
+
unless @@stale_packet_keys_warned.include?(key)
|
|
1249
|
+
@@stale_packet_keys_warned.add(key)
|
|
1250
|
+
Logger.warn("Stale tlmcnt Redis key detected for unknown packet #{key} - ignoring")
|
|
1251
|
+
end
|
|
1252
|
+
end
|
|
1244
1253
|
end
|
|
1245
1254
|
end
|
|
1246
1255
|
get_all_telemetry_counts('UNKNOWN', scope: scope).each do |packet_name, count|
|
|
1247
|
-
|
|
1248
|
-
|
|
1256
|
+
begin
|
|
1257
|
+
update_packet = System.telemetry.packet('UNKNOWN', packet_name)
|
|
1258
|
+
update_packet.received_count = count.to_i
|
|
1259
|
+
rescue RuntimeError
|
|
1260
|
+
key = "UNKNOWN #{packet_name}"
|
|
1261
|
+
unless @@stale_packet_keys_warned.include?(key)
|
|
1262
|
+
@@stale_packet_keys_warned.add(key)
|
|
1263
|
+
Logger.warn("Stale tlmcnt Redis key detected for unknown packet #{key} - ignoring")
|
|
1264
|
+
end
|
|
1265
|
+
end
|
|
1249
1266
|
end
|
|
1250
1267
|
end
|
|
1251
1268
|
|
|
@@ -1291,14 +1308,30 @@ module OpenC3
|
|
|
1291
1308
|
end
|
|
1292
1309
|
tlm_target_names.each do |target_name|
|
|
1293
1310
|
result[inc_count].each do |packet_name, count|
|
|
1294
|
-
|
|
1295
|
-
|
|
1311
|
+
begin
|
|
1312
|
+
update_packet = System.telemetry.packet(target_name, packet_name)
|
|
1313
|
+
update_packet.received_count = count.to_i
|
|
1314
|
+
rescue RuntimeError
|
|
1315
|
+
key = "#{target_name} #{packet_name}"
|
|
1316
|
+
unless @@stale_packet_keys_warned.include?(key)
|
|
1317
|
+
@@stale_packet_keys_warned.add(key)
|
|
1318
|
+
Logger.warn("Stale tlmcnt Redis key detected for unknown packet #{key} - ignoring")
|
|
1319
|
+
end
|
|
1320
|
+
end
|
|
1296
1321
|
end
|
|
1297
1322
|
inc_count += 1
|
|
1298
1323
|
end
|
|
1299
1324
|
result[inc_count].each do |packet_name, count|
|
|
1300
|
-
|
|
1301
|
-
|
|
1325
|
+
begin
|
|
1326
|
+
update_packet = System.telemetry.packet('UNKNOWN', packet_name)
|
|
1327
|
+
update_packet.received_count = count.to_i
|
|
1328
|
+
rescue RuntimeError
|
|
1329
|
+
key = "UNKNOWN #{packet_name}"
|
|
1330
|
+
unless @@stale_packet_keys_warned.include?(key)
|
|
1331
|
+
@@stale_packet_keys_warned.add(key)
|
|
1332
|
+
Logger.warn("Stale tlmcnt Redis key detected for unknown packet #{key} - ignoring")
|
|
1333
|
+
end
|
|
1334
|
+
end
|
|
1302
1335
|
end
|
|
1303
1336
|
end
|
|
1304
1337
|
end
|