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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +66 -10
  3. data/data/config/command_modifiers.yaml +3 -3
  4. data/data/config/interface_modifiers.yaml +1 -1
  5. data/data/config/param_item_modifiers.yaml +3 -0
  6. data/data/config/table_manager.yaml +1 -1
  7. data/data/config/telemetry_modifiers.yaml +3 -3
  8. data/data/config/widgets.yaml +2 -2
  9. data/lib/openc3/accessors.rb +1 -1
  10. data/lib/openc3/api/cmd_api.rb +5 -45
  11. data/lib/openc3/api/settings_api.rb +8 -0
  12. data/lib/openc3/api/stash_api.rb +1 -1
  13. data/lib/openc3/api/tlm_api.rb +93 -14
  14. data/lib/openc3/core_ext/kernel.rb +3 -3
  15. data/lib/openc3/logs/log_writer.rb +16 -12
  16. data/lib/openc3/microservices/interface_microservice.rb +2 -0
  17. data/lib/openc3/microservices/plugin_microservice.rb +2 -2
  18. data/lib/openc3/models/cvt_model.rb +140 -3
  19. data/lib/openc3/models/plugin_model.rb +7 -2
  20. data/lib/openc3/models/plugin_store_model.rb +70 -0
  21. data/lib/openc3/models/target_model.rb +26 -0
  22. data/lib/openc3/models/tool_model.rb +1 -1
  23. data/lib/openc3/packets/commands.rb +7 -49
  24. data/lib/openc3/packets/packet.rb +67 -2
  25. data/lib/openc3/packets/packet_config.rb +10 -2
  26. data/lib/openc3/packets/packet_item.rb +10 -1
  27. data/lib/openc3/packets/parsers/state_parser.rb +7 -1
  28. data/lib/openc3/packets/structure.rb +9 -2
  29. data/lib/openc3/script/calendar.rb +10 -10
  30. data/lib/openc3/script/commands.rb +37 -24
  31. data/lib/openc3/script/script_runner.rb +7 -2
  32. data/lib/openc3/script/tables.rb +3 -3
  33. data/lib/openc3/tools/table_manager/table_config.rb +3 -3
  34. data/lib/openc3/top_level.rb +2 -6
  35. data/lib/openc3/topics/command_topic.rb +12 -15
  36. data/lib/openc3/utilities/authorization.rb +1 -1
  37. data/lib/openc3/utilities/cmd_log.rb +70 -0
  38. data/lib/openc3/utilities/cosmos_rails_formatter.rb +1 -1
  39. data/lib/openc3/utilities/logger.rb +1 -1
  40. data/lib/openc3/utilities/running_script.rb +5 -1
  41. data/lib/openc3/version.rb +6 -6
  42. data/templates/tool_angular/package.json +2 -2
  43. data/templates/tool_react/package.json +1 -1
  44. data/templates/tool_svelte/package.json +1 -1
  45. data/templates/tool_vue/package.json +3 -3
  46. data/templates/widget/package.json +2 -2
  47. 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 2022, OpenC3, Inc.
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
- raise "Item '#{target_name} #{packet_name} #{value_keys[-1]}' does not exist"
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
- model = PluginModel.new(name: gem_name, variables: variables, plugin_txt_lines: plugin_txt_lines, scope: scope)
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 !@url.start_with?('http')
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
- if raw
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
- output_string << (target_name + ' ' + cmd_name)
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
- if value.is_a?(String)
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 => e
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 2024, OpenC3, Inc.
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 2022, OpenC3, Inc.
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