openc3 6.5.1 → 6.7.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 +66 -10
- data/data/config/command_modifiers.yaml +3 -3
- data/data/config/interface_modifiers.yaml +1 -1
- data/data/config/param_item_modifiers.yaml +3 -0
- data/data/config/table_manager.yaml +1 -1
- data/data/config/telemetry_modifiers.yaml +3 -3
- data/data/config/widgets.yaml +2 -2
- data/lib/openc3/accessors.rb +1 -1
- data/lib/openc3/api/cmd_api.rb +5 -45
- data/lib/openc3/api/settings_api.rb +8 -0
- data/lib/openc3/api/stash_api.rb +1 -1
- data/lib/openc3/api/tlm_api.rb +93 -14
- data/lib/openc3/core_ext/kernel.rb +3 -3
- data/lib/openc3/logs/log_writer.rb +16 -12
- data/lib/openc3/microservices/interface_microservice.rb +2 -0
- data/lib/openc3/microservices/plugin_microservice.rb +2 -2
- data/lib/openc3/models/cvt_model.rb +140 -3
- data/lib/openc3/models/plugin_model.rb +7 -2
- data/lib/openc3/models/plugin_store_model.rb +70 -0
- data/lib/openc3/models/target_model.rb +26 -0
- data/lib/openc3/models/tool_model.rb +1 -1
- data/lib/openc3/packets/commands.rb +7 -49
- data/lib/openc3/packets/packet.rb +67 -2
- data/lib/openc3/packets/packet_config.rb +10 -2
- data/lib/openc3/packets/packet_item.rb +10 -1
- data/lib/openc3/packets/parsers/state_parser.rb +7 -1
- data/lib/openc3/packets/structure.rb +9 -2
- data/lib/openc3/script/calendar.rb +10 -10
- data/lib/openc3/script/commands.rb +37 -24
- data/lib/openc3/script/script_runner.rb +7 -2
- data/lib/openc3/script/tables.rb +3 -3
- data/lib/openc3/tools/table_manager/table_config.rb +3 -3
- data/lib/openc3/top_level.rb +2 -6
- data/lib/openc3/topics/command_topic.rb +12 -15
- data/lib/openc3/utilities/authorization.rb +1 -1
- data/lib/openc3/utilities/cmd_log.rb +70 -0
- data/lib/openc3/utilities/cosmos_rails_formatter.rb +1 -1
- data/lib/openc3/utilities/logger.rb +1 -1
- data/lib/openc3/utilities/running_script.rb +5 -1
- data/lib/openc3/version.rb +6 -6
- 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 +60 -16
@@ -14,12 +14,14 @@
|
|
14
14
|
# GNU Affero General Public License for more details.
|
15
15
|
|
16
16
|
# Modified by OpenC3, Inc.
|
17
|
-
# All changes Copyright
|
17
|
+
# All changes Copyright 2025, OpenC3, Inc.
|
18
18
|
# All Rights Reserved
|
19
19
|
#
|
20
20
|
# This file may also be used under the terms of a commercial license
|
21
21
|
# if purchased from OpenC3, Inc.
|
22
22
|
|
23
|
+
require 'pg'
|
24
|
+
require 'thread'
|
23
25
|
require 'openc3/utilities/store'
|
24
26
|
require 'openc3/utilities/store_queued'
|
25
27
|
require 'openc3/models/target_model'
|
@@ -28,6 +30,8 @@ module OpenC3
|
|
28
30
|
class CvtModel
|
29
31
|
@@packet_cache = {}
|
30
32
|
@@override_cache = {}
|
33
|
+
@@conn = nil
|
34
|
+
@@conn_mutex = Mutex.new
|
31
35
|
|
32
36
|
VALUE_TYPES = [:RAW, :CONVERTED, :FORMATTED, :WITH_UNITS]
|
33
37
|
def self.build_json_from_packet(packet)
|
@@ -117,22 +121,149 @@ module OpenC3
|
|
117
121
|
end
|
118
122
|
end
|
119
123
|
|
124
|
+
def self.tsdb_lookup(items, start_time:, end_time: nil)
|
125
|
+
tables = {}
|
126
|
+
names = []
|
127
|
+
nil_count = 0
|
128
|
+
items.each do |item|
|
129
|
+
target_name, packet_name, item_name, value_type, limits = item
|
130
|
+
# They will all be nil when item is a nil value
|
131
|
+
# A nil value indicates a value that does not exist as returned by get_tlm_available
|
132
|
+
if item_name.nil?
|
133
|
+
# We know timestamp always exists so we can use it to fill in the nil value
|
134
|
+
names << "timestamp as __nil#{nil_count}"
|
135
|
+
nil_count += 1
|
136
|
+
next
|
137
|
+
end
|
138
|
+
# See https://questdb.com/docs/reference/api/ilp/advanced-settings/#name-restrictions
|
139
|
+
table_name = "#{target_name}__#{packet_name}".gsub(/[?,'"\/:\)\(\+\*\%~]/, '_')
|
140
|
+
tables[table_name] = 1
|
141
|
+
index = tables.find_index {|k,v| k == table_name }
|
142
|
+
# See https://questdb.com/docs/reference/api/ilp/advanced-settings/#name-restrictions
|
143
|
+
# NOTE: Semicolon added as it appears invalid
|
144
|
+
item_name = item_name.gsub(/[?\.,'"\\\/:\)\(\+\-\*\%~;]/, '_')
|
145
|
+
case value_type
|
146
|
+
when 'WITH_UNITS'
|
147
|
+
names << "\"T#{index}.#{item_name}__U\""
|
148
|
+
when 'FORMATTED'
|
149
|
+
names << "\"T#{index}.#{item_name}__F\""
|
150
|
+
when 'CONVERTED'
|
151
|
+
names << "\"T#{index}.#{item_name}__C\""
|
152
|
+
else
|
153
|
+
names << "\"T#{index}.#{item_name}\""
|
154
|
+
end
|
155
|
+
if limits
|
156
|
+
names << "\"T#{index}.#{item_name}__L\""
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Build the SQL query
|
161
|
+
query = "SELECT #{names.join(", ")} FROM "
|
162
|
+
tables.each_with_index do |(table_name, _), index|
|
163
|
+
if index == 0
|
164
|
+
query += "#{table_name} as T#{index} "
|
165
|
+
else
|
166
|
+
query += "ASOF JOIN #{table_name} as T#{index} "
|
167
|
+
end
|
168
|
+
end
|
169
|
+
if start_time && !end_time
|
170
|
+
query += "WHERE T0.timestamp < '#{start_time}' LIMIT -1"
|
171
|
+
elsif start_time && end_time
|
172
|
+
query += "WHERE T0.timestamp >= '#{start_time}' AND T0.timestamp < '#{end_time}'"
|
173
|
+
end
|
174
|
+
|
175
|
+
retry_count = 0
|
176
|
+
begin
|
177
|
+
@@conn_mutex.synchronize do
|
178
|
+
@@conn ||= PG::Connection.new(host: ENV['OPENC3_TSDB_HOSTNAME'],
|
179
|
+
port: ENV['OPENC3_TSDB_QUERY_PORT'],
|
180
|
+
user: ENV['OPENC3_TSDB_USERNAME'],
|
181
|
+
password: ENV['OPENC3_TSDB_PASSWORD'],
|
182
|
+
dbname: 'qdb')
|
183
|
+
# Default connection is all strings but we want to map to the correct types
|
184
|
+
if @@conn.type_map_for_results.is_a? PG::TypeMapAllStrings
|
185
|
+
# TODO: This doesn't seem to be round tripping UINT64 correctly
|
186
|
+
# Try playback with P_2.2,2 and P(:6;): from the DEMO
|
187
|
+
@@conn.type_map_for_results = PG::BasicTypeMapForResults.new @@conn
|
188
|
+
end
|
189
|
+
|
190
|
+
result = @@conn.exec(query)
|
191
|
+
if result.nil? or result.ntuples == 0
|
192
|
+
return {}
|
193
|
+
else
|
194
|
+
data = []
|
195
|
+
# Build up a results set that is an array of arrays
|
196
|
+
# Each nested array is a set of 2 items: [value, limits state]
|
197
|
+
# If the item does not have limits the limits state is nil
|
198
|
+
result.each_with_index do |tuples, index|
|
199
|
+
data[index] ||= []
|
200
|
+
row_index = 0
|
201
|
+
tuples.each do |tuple|
|
202
|
+
if tuple[0].include?("__L")
|
203
|
+
data[index][row_index - 1][1] = tuple[1]
|
204
|
+
elsif tuple[0] =~ /^__nil/
|
205
|
+
data[index][row_index] = [nil, nil]
|
206
|
+
row_index += 1
|
207
|
+
else
|
208
|
+
data[index][row_index] = [tuple[1], nil]
|
209
|
+
row_index += 1
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
# If we only have one row then we return a single array
|
214
|
+
if result.ntuples == 1
|
215
|
+
data = data[0]
|
216
|
+
end
|
217
|
+
return data
|
218
|
+
end
|
219
|
+
end
|
220
|
+
rescue IOError, PG::Error => e
|
221
|
+
# Retry the query because various errors can occur that are recoverable
|
222
|
+
retry_count += 1
|
223
|
+
if retry_count > 4
|
224
|
+
# After the 5th retry just raise the error
|
225
|
+
raise "Error querying QuestDB: #{e.message}"
|
226
|
+
end
|
227
|
+
Logger.warn("QuestDB: Retrying due to error: #{e.message}")
|
228
|
+
Logger.warn("QuestDB: Last query: #{query}") # Log the last query for debugging
|
229
|
+
@@conn_mutex.synchronize do
|
230
|
+
if @@conn and !@@conn.finished?
|
231
|
+
@@conn.finish()
|
232
|
+
end
|
233
|
+
@@conn = nil # Force the new connection
|
234
|
+
end
|
235
|
+
sleep 0.1
|
236
|
+
retry
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
120
240
|
# Return all item values and limit state from the CVT
|
121
241
|
#
|
122
242
|
# @param items [Array<String>] Items to return. Must be formatted as TGT__PKT__ITEM__TYPE
|
123
243
|
# @param stale_time [Integer] Time in seconds from Time.now that value will be marked stale
|
124
244
|
# @return [Array] Array of values
|
125
|
-
def self.get_tlm_values(items, stale_time: 30, cache_timeout: nil, scope: $openc3_scope)
|
245
|
+
def self.get_tlm_values(items, stale_time: 30, cache_timeout: nil, start_time: nil, end_time: nil, scope: $openc3_scope)
|
126
246
|
now = Time.now
|
127
247
|
results = []
|
128
248
|
lookups = []
|
129
249
|
packet_lookup = {}
|
130
250
|
overrides = {}
|
251
|
+
|
252
|
+
# If a start_time is passed we're doing a QuestDB lookup and directly return the results
|
253
|
+
# TODO: This currently does NOT support the override values
|
254
|
+
if start_time
|
255
|
+
return tsdb_lookup(items, start_time: start_time, end_time: end_time)
|
256
|
+
end
|
257
|
+
|
131
258
|
# First generate a lookup hash of all the items represented so we can query the CVT
|
132
259
|
items.each { |item| _parse_item(now, lookups, overrides, item, cache_timeout: cache_timeout, scope: scope) }
|
133
260
|
|
134
261
|
now = now.to_f
|
135
262
|
lookups.each do |target_packet_key, target_name, packet_name, value_keys|
|
263
|
+
if target_packet_key.nil?
|
264
|
+
results << [nil, nil]
|
265
|
+
next
|
266
|
+
end
|
136
267
|
unless packet_lookup[target_packet_key]
|
137
268
|
packet_lookup[target_packet_key] = get(target_name: target_name, packet_name: packet_name, cache_timeout: cache_timeout, scope: scope)
|
138
269
|
end
|
@@ -161,7 +292,8 @@ module OpenC3
|
|
161
292
|
if hash.key?(value_keys[-1])
|
162
293
|
item_result[1] = nil
|
163
294
|
else
|
164
|
-
|
295
|
+
item_result[0] = nil
|
296
|
+
item_result[1] = nil
|
165
297
|
end
|
166
298
|
end
|
167
299
|
end
|
@@ -335,6 +467,11 @@ module OpenC3
|
|
335
467
|
# return an ordered array of hash with keys
|
336
468
|
def self._parse_item(now, lookups, overrides, item, cache_timeout:, scope:)
|
337
469
|
target_name, packet_name, item_name, value_type = item
|
470
|
+
# They will all be nil when item is a nil value
|
471
|
+
if item_name.nil?
|
472
|
+
lookups << nil
|
473
|
+
return
|
474
|
+
end
|
338
475
|
|
339
476
|
# We build lookup keys by including all the less formatted types to gracefully degrade lookups
|
340
477
|
# This allows the user to specify WITH_UNITS and if there is no conversions it will simply return the RAW value
|
@@ -55,6 +55,7 @@ module OpenC3
|
|
55
55
|
attr_accessor :variables
|
56
56
|
attr_accessor :plugin_txt_lines
|
57
57
|
attr_accessor :needs_dependencies
|
58
|
+
attr_accessor :store_id
|
58
59
|
|
59
60
|
# NOTE: The following three class methods are used by the ModelController
|
60
61
|
# and are reimplemented to enable various Model class methods to work
|
@@ -72,7 +73,7 @@ module OpenC3
|
|
72
73
|
|
73
74
|
# Called by the PluginsController to parse the plugin variables
|
74
75
|
# Doesn't actually create the plugin during the phase
|
75
|
-
def self.install_phase1(gem_file_path, existing_variables: nil, existing_plugin_txt_lines: nil, process_existing: false, scope:, validate_only: false)
|
76
|
+
def self.install_phase1(gem_file_path, existing_variables: nil, existing_plugin_txt_lines: nil, store_id: nil, process_existing: false, scope:, validate_only: false)
|
76
77
|
gem_name = File.basename(gem_file_path).split("__")[0]
|
77
78
|
|
78
79
|
temp_dir = Dir.mktmpdir
|
@@ -130,7 +131,8 @@ module OpenC3
|
|
130
131
|
end
|
131
132
|
end
|
132
133
|
|
133
|
-
|
134
|
+
store_id = Integer(store_id) if store_id
|
135
|
+
model = PluginModel.new(name: gem_name, variables: variables, plugin_txt_lines: plugin_txt_lines, store_id: store_id, scope: scope)
|
134
136
|
result = model.as_json(:allow_nan => true)
|
135
137
|
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']
|
136
138
|
return result
|
@@ -299,6 +301,7 @@ module OpenC3
|
|
299
301
|
variables: {},
|
300
302
|
plugin_txt_lines: [],
|
301
303
|
needs_dependencies: false,
|
304
|
+
store_id: nil,
|
302
305
|
updated_at: nil,
|
303
306
|
scope:
|
304
307
|
)
|
@@ -306,6 +309,7 @@ module OpenC3
|
|
306
309
|
@variables = variables
|
307
310
|
@plugin_txt_lines = plugin_txt_lines
|
308
311
|
@needs_dependencies = ConfigParser.handle_true_false(needs_dependencies)
|
312
|
+
@store_id = store_id
|
309
313
|
end
|
310
314
|
|
311
315
|
def create(update: false, force: false, queued: false)
|
@@ -319,6 +323,7 @@ module OpenC3
|
|
319
323
|
'variables' => @variables,
|
320
324
|
'plugin_txt_lines' => @plugin_txt_lines,
|
321
325
|
'needs_dependencies' => @needs_dependencies,
|
326
|
+
'store_id' => @store_id,
|
322
327
|
'updated_at' => @updated_at
|
323
328
|
}
|
324
329
|
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# encoding: ascii-8bit
|
2
|
+
|
3
|
+
# Copyright 2025 OpenC3, Inc.
|
4
|
+
# All Rights Reserved.
|
5
|
+
#
|
6
|
+
# This program is free software; you can modify and/or redistribute it
|
7
|
+
# under the terms of the GNU Affero General Public License
|
8
|
+
# as published by the Free Software Foundation; version 3 with
|
9
|
+
# attribution addendums as found in the LICENSE.txt
|
10
|
+
#
|
11
|
+
# This program is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU Affero General Public License for more details.
|
15
|
+
|
16
|
+
# This file may also be used under the terms of a commercial license
|
17
|
+
# if purchased from OpenC3, Inc.
|
18
|
+
|
19
|
+
require 'openc3/models/model'
|
20
|
+
require 'openc3/utilities/store'
|
21
|
+
|
22
|
+
module OpenC3
|
23
|
+
class PluginStoreModel < Model
|
24
|
+
PRIMARY_KEY = 'openc3_plugin_store'
|
25
|
+
|
26
|
+
def self.set(plugin_store_data)
|
27
|
+
Store.set(PRIMARY_KEY, plugin_store_data)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.all()
|
31
|
+
Store.get(PRIMARY_KEY)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.plugin_store_error(message)
|
35
|
+
Store.set(PRIMARY_KEY, [{
|
36
|
+
date: Time.now.utc.iso8601,
|
37
|
+
title: 'Plugin Store Error',
|
38
|
+
body: message,
|
39
|
+
error: true,
|
40
|
+
}].to_json)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.get_by_id(id)
|
44
|
+
plugins = JSON.parse(all()) rescue []
|
45
|
+
plugins.find { |plugin| plugin["id"] == Integer(id) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.update
|
49
|
+
setting = SettingModel.get(name: 'store_url', scope: 'DEFAULT')
|
50
|
+
store_url = setting['data'] if setting
|
51
|
+
store_url = 'https://store.openc3.com' if store_url.nil? or store_url.strip.empty?
|
52
|
+
conn = Faraday.new(
|
53
|
+
url: store_url,
|
54
|
+
)
|
55
|
+
response = conn.get('/cosmos_plugins/json')
|
56
|
+
if response.success?
|
57
|
+
self.set(response.body)
|
58
|
+
else
|
59
|
+
self.plugin_store_error("Error contacting plugin store at #{store_url} (status: #{response.status})")
|
60
|
+
end
|
61
|
+
rescue Exception => e
|
62
|
+
self.plugin_store_error("Error contacting plugin store at #{store_url}. #{e.message})")
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.ensure_exists
|
66
|
+
plugins = self.all()
|
67
|
+
self.update() if plugins.nil? or plugins.length.zero? or plugins[0]['error']
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -1088,6 +1088,25 @@ module OpenC3
|
|
1088
1088
|
Logger.info "Configured microservice #{microservice_name}"
|
1089
1089
|
end
|
1090
1090
|
|
1091
|
+
def deploy_tsdb_microservice(gem_path, variables, topics, instance = nil, parent = nil)
|
1092
|
+
microservice_name = "#{@scope}__TSDB#{instance}__#{@name}"
|
1093
|
+
microservice = MicroserviceModel.new(
|
1094
|
+
name: microservice_name,
|
1095
|
+
folder_name: @folder_name,
|
1096
|
+
cmd: ["python", "quest_microservice.py", microservice_name],
|
1097
|
+
work_dir: "/openc3/python/openc3/microservices",
|
1098
|
+
topics: topics,
|
1099
|
+
plugin: @plugin,
|
1100
|
+
parent: nil,
|
1101
|
+
needs_dependencies: @needs_dependencies,
|
1102
|
+
shard: @shard,
|
1103
|
+
scope: @scope
|
1104
|
+
)
|
1105
|
+
microservice.create
|
1106
|
+
microservice.deploy(gem_path, variables)
|
1107
|
+
Logger.info "Configured microservice #{microservice_name}"
|
1108
|
+
end
|
1109
|
+
|
1091
1110
|
def deploy_reducer_microservice(gem_path, variables, topics, instance = nil, parent = nil)
|
1092
1111
|
microservice_name = "#{@scope}__REDUCER#{instance}__#{@name}"
|
1093
1112
|
microservice = MicroserviceModel.new(
|
@@ -1250,6 +1269,13 @@ module OpenC3
|
|
1250
1269
|
deploy_decom_microservice(system.targets[@name], gem_path, variables, topics, instance, parent)
|
1251
1270
|
end
|
1252
1271
|
|
1272
|
+
# TSDB Microservice
|
1273
|
+
if ENV['OPENC3_TSDB_HOSTNAME'] and ENV['OPENC3_TSDB_QUERY_PORT'] and ENV['OPENC3_TSDB_INGEST_PORT'] and ENV['OPENC3_TSDB_USERNAME'] and ENV['OPENC3_TSDB_PASSWORD']
|
1274
|
+
deploy_target_microservices('TSDB', decom_topic_list, "#{@scope}__DECOM__{#{@name}}") do |topics, instance, parent|
|
1275
|
+
deploy_tsdb_microservice(gem_path, variables, topics, instance, parent)
|
1276
|
+
end
|
1277
|
+
end
|
1278
|
+
|
1253
1279
|
# Reducer Microservice
|
1254
1280
|
unless @reducer_disable
|
1255
1281
|
# TODO: Does Reducer even need a topic list?
|
@@ -184,7 +184,7 @@ module OpenC3
|
|
184
184
|
end
|
185
185
|
end
|
186
186
|
|
187
|
-
if @url and !@url.start_with?('/') and
|
187
|
+
if @url and !@url.start_with?('/') and @url !~ URI::regexp
|
188
188
|
raise "URL must be a full URL (http://domain.com/path) or a relative path (/path)"
|
189
189
|
end
|
190
190
|
|
@@ -24,6 +24,7 @@
|
|
24
24
|
# See https://github.com/OpenC3/cosmos/pull/1963
|
25
25
|
|
26
26
|
require 'openc3/packets/packet_config'
|
27
|
+
require 'openc3/utilities/cmd_log'
|
27
28
|
|
28
29
|
module OpenC3
|
29
30
|
# Commands uses PacketConfig to parse the command and telemetry
|
@@ -37,6 +38,7 @@ module OpenC3
|
|
37
38
|
# Packet or PacketItem objects. While there are some overlapping methods between
|
38
39
|
# the two, these are separate interfaces into the system.
|
39
40
|
class Commands
|
41
|
+
include OpenC3::CmdLog
|
40
42
|
attr_accessor :config
|
41
43
|
|
42
44
|
LATEST_PACKET_NAME = 'LATEST'.freeze
|
@@ -203,60 +205,16 @@ module OpenC3
|
|
203
205
|
raw = false
|
204
206
|
end
|
205
207
|
items.delete_if { |item_name, _item_value| ignored_parameters.include?(item_name) }
|
206
|
-
return build_cmd_output_string(packet.target_name, packet.packet_name, items, raw)
|
208
|
+
return build_cmd_output_string(packet.target_name, packet.packet_name, items, raw, packet)
|
207
209
|
end
|
208
210
|
|
209
|
-
def build_cmd_output_string(target_name, cmd_name, cmd_params, raw = false)
|
210
|
-
|
211
|
-
output_string = 'cmd_raw("'
|
212
|
-
else
|
213
|
-
output_string = 'cmd("'
|
214
|
-
end
|
211
|
+
def build_cmd_output_string(target_name, cmd_name, cmd_params, raw = false, packet)
|
212
|
+
method_name = raw ? "cmd_raw" : "cmd"
|
215
213
|
target_name = 'UNKNOWN' unless target_name
|
216
214
|
cmd_name = 'UNKNOWN' unless cmd_name
|
217
|
-
|
218
|
-
if cmd_params.nil? or cmd_params.empty?
|
219
|
-
output_string << '")'
|
220
|
-
else
|
221
|
-
begin
|
222
|
-
command_items = packet(target_name, cmd_name).items
|
223
|
-
rescue
|
224
|
-
end
|
225
|
-
|
226
|
-
params = []
|
227
|
-
cmd_params.each do |key, value|
|
228
|
-
next if Packet::RESERVED_ITEM_NAMES.include?(key)
|
229
|
-
|
230
|
-
begin
|
231
|
-
item_type = command_items[key].data_type
|
232
|
-
rescue
|
233
|
-
item_type = nil
|
234
|
-
end
|
215
|
+
packet_hash = packet ? packet.as_json : {}
|
235
216
|
|
236
|
-
|
237
|
-
value = value.dup
|
238
|
-
if item_type == :BLOCK or item_type == :STRING
|
239
|
-
if !value.is_printable?
|
240
|
-
value = "0x" + value.simple_formatted
|
241
|
-
else
|
242
|
-
value = value.inspect
|
243
|
-
end
|
244
|
-
else
|
245
|
-
value = value.convert_to_value.to_s
|
246
|
-
end
|
247
|
-
if value.length > 256
|
248
|
-
value = value[0..255] + "...'"
|
249
|
-
end
|
250
|
-
value.tr!('"', "'")
|
251
|
-
elsif value.is_a?(Array)
|
252
|
-
value = "[#{value.join(", ")}]"
|
253
|
-
end
|
254
|
-
params << "#{key} #{value}"
|
255
|
-
end
|
256
|
-
params = params.join(", ")
|
257
|
-
output_string << (' with ' + params + '")')
|
258
|
-
end
|
259
|
-
return output_string
|
217
|
+
_build_cmd_output_string(method_name, target_name, cmd_name, cmd_params, packet_hash)
|
260
218
|
end
|
261
219
|
|
262
220
|
# Returns whether the given command is hazardous. Commands are hazardous
|
@@ -156,6 +156,7 @@ module OpenC3
|
|
156
156
|
@virtual = false
|
157
157
|
@restricted = false
|
158
158
|
@validator = nil
|
159
|
+
@obfuscated_items = []
|
159
160
|
end
|
160
161
|
|
161
162
|
# Sets the target name this packet is associated with. Unidentified packets
|
@@ -333,7 +334,7 @@ module OpenC3
|
|
333
334
|
synchronize() do
|
334
335
|
begin
|
335
336
|
internal_buffer_equals(buffer)
|
336
|
-
rescue RuntimeError
|
337
|
+
rescue RuntimeError
|
337
338
|
Logger.instance.error "#{@target_name} #{@packet_name} received with actual packet length of #{buffer.length} but defined length of #{@defined_length}"
|
338
339
|
end
|
339
340
|
@read_conversion_cache.clear if @read_conversion_cache
|
@@ -556,6 +557,7 @@ module OpenC3
|
|
556
557
|
item = super(item)
|
557
558
|
update_id_items(item)
|
558
559
|
update_limits_items_cache(item)
|
560
|
+
update_obfuscated_items_cache(item)
|
559
561
|
item
|
560
562
|
end
|
561
563
|
|
@@ -905,6 +907,7 @@ module OpenC3
|
|
905
907
|
@short_buffer_allowed = false
|
906
908
|
@id_items = nil
|
907
909
|
@limits_items = nil
|
910
|
+
@obfuscated_items = []
|
908
911
|
new_items = {}
|
909
912
|
new_sorted_items = []
|
910
913
|
@items.each do |name, item|
|
@@ -957,6 +960,21 @@ module OpenC3
|
|
957
960
|
end
|
958
961
|
end
|
959
962
|
|
963
|
+
# Add an item to the obfuscate items cache if necessary.
|
964
|
+
# You MUST call this after adding obfuscation to an item
|
965
|
+
# This is an optimization so we don't have to iterate through all the items when
|
966
|
+
# checking for obfuscation.
|
967
|
+
def update_obfuscated_items_cache(item)
|
968
|
+
if item.obfuscate
|
969
|
+
@obfuscated_items ||= []
|
970
|
+
@obfuscated_items_hash ||= {}
|
971
|
+
unless @obfuscated_items_hash[item]
|
972
|
+
@obfuscated_items << item
|
973
|
+
@obfuscated_items_hash[item] = true
|
974
|
+
end
|
975
|
+
end
|
976
|
+
end
|
977
|
+
|
960
978
|
# Return an array of arrays indicating all items in the packet that are out of limits
|
961
979
|
# [[target name, packet name, item name, item limits state], ...]
|
962
980
|
#
|
@@ -1158,6 +1176,7 @@ module OpenC3
|
|
1158
1176
|
config['validator'] = @validator.class.to_s if @validator
|
1159
1177
|
config['template'] = Base64.encode64(@template) if @template
|
1160
1178
|
config['config_name'] = self.config_name
|
1179
|
+
config['obfuscated_items'] = @obfuscated_items&.map(&:name) || []
|
1161
1180
|
|
1162
1181
|
if @processors
|
1163
1182
|
processors = []
|
@@ -1287,6 +1306,52 @@ module OpenC3
|
|
1287
1306
|
end
|
1288
1307
|
end
|
1289
1308
|
|
1309
|
+
def obfuscate()
|
1310
|
+
return unless @buffer
|
1311
|
+
return unless @obfuscated_items
|
1312
|
+
|
1313
|
+
@obfuscated_items.each do |item|
|
1314
|
+
next if item.data_type == :DERIVED
|
1315
|
+
|
1316
|
+
begin
|
1317
|
+
current_value = read(item.name, :RAW)
|
1318
|
+
|
1319
|
+
case current_value
|
1320
|
+
when Array
|
1321
|
+
# For arrays, create a new array of zeros with the same size
|
1322
|
+
case item.data_type
|
1323
|
+
when :INT, :UINT
|
1324
|
+
obfuscated_value = Array.new(current_value.size, 0)
|
1325
|
+
when :FLOAT
|
1326
|
+
obfuscated_value = Array.new(current_value.size, 0.0)
|
1327
|
+
when :STRING, :BLOCK
|
1328
|
+
obfuscated_value = Array.new(current_value.size) { |i|
|
1329
|
+
"\x00" * current_value[i].length if current_value[i]
|
1330
|
+
}
|
1331
|
+
else
|
1332
|
+
obfuscated_value = Array.new(current_value.size, 0)
|
1333
|
+
end
|
1334
|
+
when String
|
1335
|
+
# For strings/blocks, create null bytes of the same length
|
1336
|
+
obfuscated_value = "\x00" * current_value.length
|
1337
|
+
else
|
1338
|
+
case item.data_type
|
1339
|
+
when :INT, :UINT
|
1340
|
+
obfuscated_value = 0
|
1341
|
+
when :FLOAT
|
1342
|
+
obfuscated_value = 0.0
|
1343
|
+
else
|
1344
|
+
obfuscated_value = 0
|
1345
|
+
end
|
1346
|
+
end
|
1347
|
+
write(item.name, obfuscated_value, :RAW)
|
1348
|
+
rescue => e
|
1349
|
+
Logger.instance.error "#{item.name} obfuscation failed with error: #{e.message}"
|
1350
|
+
next
|
1351
|
+
end
|
1352
|
+
end
|
1353
|
+
end
|
1354
|
+
|
1290
1355
|
protected
|
1291
1356
|
|
1292
1357
|
def handle_limits_states(item, value)
|
@@ -1404,4 +1469,4 @@ module OpenC3
|
|
1404
1469
|
item
|
1405
1470
|
end
|
1406
1471
|
end
|
1407
|
-
end
|
1472
|
+
end
|
@@ -14,7 +14,7 @@
|
|
14
14
|
# GNU Affero General Public License for more details.
|
15
15
|
|
16
16
|
# Modified by OpenC3, Inc.
|
17
|
-
# All changes Copyright
|
17
|
+
# All changes Copyright 2025, OpenC3, Inc.
|
18
18
|
# All Rights Reserved
|
19
19
|
#
|
20
20
|
# This file may also be used under the terms of a commercial license
|
@@ -234,7 +234,8 @@ module OpenC3
|
|
234
234
|
'POLY_WRITE_CONVERSION', 'SEG_POLY_READ_CONVERSION', 'SEG_POLY_WRITE_CONVERSION',\
|
235
235
|
'GENERIC_READ_CONVERSION_START', 'GENERIC_WRITE_CONVERSION_START', 'REQUIRED',\
|
236
236
|
'LIMITS', 'LIMITS_RESPONSE', 'UNITS', 'FORMAT_STRING', 'DESCRIPTION',\
|
237
|
-
'MINIMUM_VALUE', 'MAXIMUM_VALUE', 'DEFAULT_VALUE', 'OVERFLOW', 'OVERLAP', 'KEY', 'VARIABLE_BIT_SIZE'
|
237
|
+
'MINIMUM_VALUE', 'MAXIMUM_VALUE', 'DEFAULT_VALUE', 'OVERFLOW', 'OVERLAP', 'KEY', 'VARIABLE_BIT_SIZE',\
|
238
|
+
'OBFUSCATE'
|
238
239
|
raise parser.error("No current item for #{keyword}") unless @current_item
|
239
240
|
|
240
241
|
process_current_item(parser, keyword, params)
|
@@ -692,6 +693,13 @@ module OpenC3
|
|
692
693
|
@current_item.units_full = params[0]
|
693
694
|
@current_item.units = params[1]
|
694
695
|
|
696
|
+
# Obfuscate the parameter in logs
|
697
|
+
when 'OBFUSCATE'
|
698
|
+
usage = "OBFUSCATE"
|
699
|
+
parser.verify_num_parameters(0, 0, usage)
|
700
|
+
@current_item.obfuscate = true
|
701
|
+
@current_packet.update_obfuscated_items_cache(@current_item)
|
702
|
+
|
695
703
|
# Update the description for the current telemetry item
|
696
704
|
when 'DESCRIPTION'
|
697
705
|
usage = "DESCRIPTION <DESCRIPTION>"
|
@@ -14,7 +14,7 @@
|
|
14
14
|
# GNU Affero General Public License for more details.
|
15
15
|
|
16
16
|
# Modified by OpenC3, Inc.
|
17
|
-
# All changes Copyright
|
17
|
+
# All changes Copyright 2025, OpenC3, Inc.
|
18
18
|
# All Rights Reserved
|
19
19
|
#
|
20
20
|
# This file may also be used under the terms of a commercial license
|
@@ -93,6 +93,9 @@ module OpenC3
|
|
93
93
|
# @return [PacketItemLimits] All information regarding limits for this PacketItem
|
94
94
|
attr_reader :limits
|
95
95
|
|
96
|
+
# @return [Boolean] Whether the parameter must be obfuscated from logs or not
|
97
|
+
attr_accessor :obfuscate
|
98
|
+
|
96
99
|
# (see StructureItem#initialize)
|
97
100
|
# It also initializes the attributes of the PacketItem.
|
98
101
|
def initialize(name, bit_offset, bit_size, data_type, endianness, array_size = nil, overflow = :ERROR)
|
@@ -109,6 +112,7 @@ module OpenC3
|
|
109
112
|
@range = nil
|
110
113
|
@required = false
|
111
114
|
@hazardous = nil
|
115
|
+
@obfuscate = false
|
112
116
|
@messages_disabled = nil
|
113
117
|
@state_colors = nil
|
114
118
|
@limits = PacketItemLimits.new
|
@@ -314,6 +318,7 @@ module OpenC3
|
|
314
318
|
item.state_colors = self.state_colors.clone if self.state_colors
|
315
319
|
item.limits = self.limits.clone if self.limits
|
316
320
|
item.meta = self.meta.clone if @meta
|
321
|
+
item.obfuscate = self.obfuscate.clone if @obfuscate
|
317
322
|
item
|
318
323
|
end
|
319
324
|
alias dup clone
|
@@ -345,6 +350,7 @@ module OpenC3
|
|
345
350
|
hash['limits'] = self.limits.to_hash
|
346
351
|
hash['meta'] = nil
|
347
352
|
hash['meta'] = @meta if @meta
|
353
|
+
hash['obfuscate'] = self.obfuscate
|
348
354
|
hash
|
349
355
|
end
|
350
356
|
|
@@ -404,6 +410,7 @@ module OpenC3
|
|
404
410
|
config << " FORMAT_STRING #{self.format_string.to_s.quote_if_necessary}\n" if self.format_string
|
405
411
|
config << " UNITS #{self.units_full.to_s.quote_if_necessary} #{self.units.to_s.quote_if_necessary}\n" if self.units
|
406
412
|
config << " OVERFLOW #{self.overflow}\n" if self.overflow != :ERROR
|
413
|
+
config << " OBFUSCATE\n" if self.obfuscate
|
407
414
|
|
408
415
|
if @states
|
409
416
|
@states.each do |state_name, state_value|
|
@@ -512,6 +519,7 @@ module OpenC3
|
|
512
519
|
|
513
520
|
config['meta'] = @meta if @meta
|
514
521
|
config['variable_bit_size'] = @variable_bit_size if @variable_bit_size
|
522
|
+
config['obfuscate'] = self.obfuscate
|
515
523
|
config
|
516
524
|
end
|
517
525
|
|
@@ -573,6 +581,7 @@ module OpenC3
|
|
573
581
|
item.limits.values = values if values.length > 0
|
574
582
|
end
|
575
583
|
item.meta = hash['meta']
|
584
|
+
item.obfuscate = hash['obfuscate']
|
576
585
|
item.variable_bit_size = hash['variable_bit_size']
|
577
586
|
item
|
578
587
|
end
|
@@ -80,7 +80,13 @@ module OpenC3
|
|
80
80
|
if data_type == :STRING || data_type == :BLOCK
|
81
81
|
@parser.parameters[1]
|
82
82
|
else
|
83
|
-
@parser.parameters[1].convert_to_value
|
83
|
+
value = @parser.parameters[1].convert_to_value
|
84
|
+
# Check if the value is a string, which indicates an error in parsing
|
85
|
+
# except for 'ANY' which is a valid state value
|
86
|
+
if value.is_a?(String) and value != "ANY"
|
87
|
+
raise @parser.error("Invalid state value #{value} for data type #{data_type}.", @usage)
|
88
|
+
end
|
89
|
+
return value
|
84
90
|
end
|
85
91
|
end
|
86
92
|
|