openc3 6.2.0 → 6.3.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.
@@ -14,11 +14,17 @@
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
21
21
  # if purchased from OpenC3, Inc.
22
+ #
23
+ # A portion of this file was funded by Blue Origin Enterprises, L.P.
24
+ # See https://github.com/OpenC3/cosmos/pull/1953 and https://github.com/OpenC3/cosmos/pull/1963
25
+
26
+ # A portion of this file was funded by Blue Origin Enterprises, L.P.
27
+ # See https://github.com/OpenC3/cosmos/pull/1957
22
28
 
23
29
  require 'openc3/top_level'
24
30
  require 'openc3/models/model'
@@ -204,7 +210,7 @@ module OpenC3
204
210
  JSON.parse(json, :allow_nan => true, :create_additions => true)
205
211
  end
206
212
 
207
- # @return [Array>Hash>] All packet hashes under the target_name
213
+ # @return [Array<Hash>] All packet hashes under the target_name
208
214
  def self.packets(target_name, type: :TLM, scope:)
209
215
  raise "Unknown type #{type} for #{target_name}" unless VALID_TYPES.include?(type)
210
216
  raise "Target '#{target_name}' does not exist for scope: #{scope}" unless get(name: target_name, scope: scope)
@@ -217,7 +223,7 @@ module OpenC3
217
223
  result
218
224
  end
219
225
 
220
- # @return [Array>Hash>] All packet hashes under the target_name
226
+ # @return [Array<Hash>] All packet hashes under the target_name
221
227
  def self.all_packet_name_descriptions(target_name, type: :TLM, scope:)
222
228
  self.packets(target_name, type: type, scope: scope).map! { |hash| hash.slice("packet_name", "description") }
223
229
  end
@@ -257,6 +263,28 @@ module OpenC3
257
263
  found
258
264
  end
259
265
 
266
+ # @return [Array<String>] All the item names for every packet in a target
267
+ def self.all_item_names(target_name, type: :TLM, scope:)
268
+ items = Store.zrange("#{scope}__openc3tlm__#{target_name}__allitems", 0, -1)
269
+ items = rebuild_target_allitems_list(target_name, type: type, scope: scope) if items.empty?
270
+ items
271
+ end
272
+
273
+ def self.rebuild_target_allitems_list(target_name, type: :TLM, scope:)
274
+ packets = packets(target_name, type: type, scope: scope)
275
+ packets.each do |packet|
276
+ packet['items'].each do |item|
277
+ TargetModel.add_to_target_allitems_list(target_name, item['name'], scope: scope)
278
+ end
279
+ end
280
+ Store.zrange("#{scope}__openc3tlm__#{target_name}__allitems", 0, -1) # return the new sorted set to let redis do the sorting
281
+ end
282
+
283
+ def self.add_to_target_allitems_list(target_name, item_name, scope:)
284
+ score = 0 # https://redis.io/docs/latest/develop/data-types/sorted-sets/#lexicographical-scores
285
+ Store.zadd("#{scope}__openc3tlm__#{target_name}__allitems", score, item_name)
286
+ end
287
+
260
288
  # @return [Hash{String => Array<Array<String, String, String>>}]
261
289
  def self.limits_groups(scope:)
262
290
  groups = Store.hgetall("#{scope}__limits_groups")
@@ -310,6 +338,7 @@ module OpenC3
310
338
  end
311
339
  end
312
340
 
341
+ # Make sure to update target_model.py if you add additional parameters
313
342
  def initialize(
314
343
  name:,
315
344
  folder_name: nil,
@@ -763,7 +792,10 @@ module OpenC3
763
792
 
764
793
  def update_store_telemetry(packet_hash, clear_old: true)
765
794
  packet_hash.each do |target_name, packets|
766
- Store.del("#{@scope}__openc3tlm__#{target_name}") if clear_old
795
+ if clear_old
796
+ Store.del("#{@scope}__openc3tlm__#{target_name}")
797
+ Store.del("#{@scope}__openc3tlm__#{target_name}__allitems")
798
+ end
767
799
  packets.each do |packet_name, packet|
768
800
  Logger.debug "Configuring tlm packet: #{target_name} #{packet_name}"
769
801
  begin
@@ -775,6 +807,7 @@ module OpenC3
775
807
  json_hash = Hash.new
776
808
  packet.sorted_items.each do |item|
777
809
  json_hash[item.name] = nil
810
+ TargetModel.add_to_target_allitems_list(target_name, item.name, scope: @scope)
778
811
  end
779
812
  CvtModel.set(json_hash, target_name: packet.target_name, packet_name: packet.packet_name, scope: @scope)
780
813
  end
@@ -834,6 +867,8 @@ module OpenC3
834
867
  return system
835
868
  end
836
869
 
870
+ # NOTE: If you call dynamic_update multiple times you should specify a different
871
+ # filename parameter or the last one will be overwritten
837
872
  def dynamic_update(packets, cmd_or_tlm = :TELEMETRY, filename = "dynamic_tlm.txt")
838
873
  # Build hash of targets/packets
839
874
  packet_hash = {}
@@ -862,17 +897,15 @@ module OpenC3
862
897
  config << "\n"
863
898
  end
864
899
  configs.each do |target_name, config|
865
- begin
866
- bucket_key = "#{@scope}/targets_modified/#{target_name}/cmd_tlm/#{filename}"
867
- client = Bucket.getClient()
868
- client.put_object(
869
- # Use targets_modified to save modifications
870
- # This keeps the original target clean (read-only)
871
- bucket: ENV['OPENC3_CONFIG_BUCKET'],
872
- key: bucket_key,
873
- body: config
874
- )
875
- end
900
+ bucket_key = "#{@scope}/targets_modified/#{target_name}/cmd_tlm/#{filename}"
901
+ client = Bucket.getClient()
902
+ client.put_object(
903
+ # Use targets_modified to save modifications
904
+ # This keeps the original target clean (read-only)
905
+ bucket: ENV['OPENC3_CONFIG_BUCKET'],
906
+ key: bucket_key,
907
+ body: config
908
+ )
876
909
  end
877
910
 
878
911
  # Inform microservices of new topics
@@ -1233,5 +1266,107 @@ module OpenC3
1233
1266
  deploy_multi_microservice(gem_path, variables)
1234
1267
  end
1235
1268
  end
1269
+
1270
+ def self.increment_telemetry_count(target_name, packet_name, count, scope:)
1271
+ result = Store.hincrby("#{scope}__TELEMETRYCNTS__{#{target_name}}", packet_name, count)
1272
+ if String === result
1273
+ return result.to_i
1274
+ else
1275
+ return result
1276
+ end
1277
+ end
1278
+
1279
+ def self.get_all_telemetry_counts(target_name, scope:)
1280
+ result = {}
1281
+ get_all = Store.hgetall("#{scope}__TELEMETRYCNTS__{#{target_name}}")
1282
+ if Hash === get_all
1283
+ get_all.each do |key, value|
1284
+ result[key] = value.to_i
1285
+ end
1286
+ else
1287
+ return result
1288
+ end
1289
+ end
1290
+
1291
+ def self.get_telemetry_count(target_name, packet_name, scope:)
1292
+ value = Store.hget("#{scope}__TELEMETRYCNTS__{#{target_name}}", packet_name)
1293
+ if String === value
1294
+ return value.to_i
1295
+ elsif value.nil?
1296
+ return 0 # Return 0 if the key doesn't exist
1297
+ else
1298
+ return value
1299
+ end
1300
+ end
1301
+
1302
+ def self.get_telemetry_counts(target_packets, scope:)
1303
+ result = Store.redis_pool.pipelined do
1304
+ target_packets.each do |target_name, packet_name|
1305
+ target_name = target_name.upcase
1306
+ packet_name = packet_name.upcase
1307
+ Store.hget("#{scope}__TELEMETRYCNTS__{#{target_name}}", packet_name)
1308
+ end
1309
+ end
1310
+ counts = []
1311
+ result.each do |count|
1312
+ if count
1313
+ counts << count.to_i
1314
+ else
1315
+ counts << 0
1316
+ end
1317
+ end
1318
+ return counts
1319
+ end
1320
+
1321
+ def self.increment_command_count(target_name, packet_name, count, scope:)
1322
+ result = Store.hincrby("#{scope}__COMMANDCNTS__{#{target_name}}", packet_name, count)
1323
+ if String === result
1324
+ return result.to_i
1325
+ else
1326
+ return result
1327
+ end
1328
+ end
1329
+
1330
+ def self.get_all_command_counts(target_name, scope:)
1331
+ result = {}
1332
+ get_all = Store.hgetall("#{scope}__COMMANDCNTS__{#{target_name}}")
1333
+ if Hash === get_all
1334
+ get_all.each do |key, value|
1335
+ result[key] = value.to_i
1336
+ end
1337
+ else
1338
+ return result
1339
+ end
1340
+ end
1341
+
1342
+ def self.get_command_count(target_name, packet_name, scope:)
1343
+ value = Store.hget("#{scope}__COMMANDCNTS__{#{target_name}}", packet_name)
1344
+ if String === value
1345
+ return value.to_i
1346
+ elsif value.nil?
1347
+ return 0 # Return 0 if the key doesn't exist
1348
+ else
1349
+ return value
1350
+ end
1351
+ end
1352
+
1353
+ def self.get_command_counts(target_packets, scope:)
1354
+ result = Store.redis_pool.pipelined do
1355
+ target_packets.each do |target_name, packet_name|
1356
+ target_name = target_name.upcase
1357
+ packet_name = packet_name.upcase
1358
+ Store.hget("#{scope}__COMMANDCNTS__{#{target_name}}", packet_name)
1359
+ end
1360
+ end
1361
+ counts = []
1362
+ result.each do |count|
1363
+ if count
1364
+ counts << count.to_i
1365
+ else
1366
+ counts << 0
1367
+ end
1368
+ end
1369
+ return counts
1370
+ end
1236
1371
  end
1237
1372
  end
@@ -14,11 +14,14 @@
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
21
21
  # if purchased from OpenC3, Inc.
22
+ #
23
+ # A portion of this file was funded by Blue Origin Enterprises, L.P.
24
+ # See https://github.com/OpenC3/cosmos/pull/1963
22
25
 
23
26
  require 'openc3/packets/packet_config'
24
27
 
@@ -95,6 +98,9 @@ module OpenC3
95
98
  # an uninitialized copy of the command. Thus you must use the return value
96
99
  # of this method.
97
100
  #
101
+ # Note: this method does not increment received_count and it should be
102
+ # incremented externally if needed.
103
+ #
98
104
  # @param (see #identify_tlm!)
99
105
  # @return (see #identify_tlm!)
100
106
  def identify(packet_data, target_names = nil)
@@ -133,7 +139,6 @@ module OpenC3
133
139
  end
134
140
 
135
141
  if identified_packet
136
- identified_packet.received_count += 1
137
142
  identified_packet = identified_packet.clone
138
143
  identified_packet.received_time = nil
139
144
  identified_packet.stored = false
@@ -149,6 +154,9 @@ module OpenC3
149
154
  # Returns a copy of the specified command packet with the parameters
150
155
  # initialized to the given params values.
151
156
  #
157
+ # Note: this method does not increment received_count and it should be
158
+ # incremented externally if needed.
159
+ #
152
160
  # @param target_name (see #packet)
153
161
  # @param packet_name (see #packet)
154
162
  # @param params [Hash<param_name=>param_value>] Parameter items to override
@@ -164,7 +172,6 @@ module OpenC3
164
172
 
165
173
  # Lookup the command and create a light weight copy
166
174
  pkt = packet(target_upcase, packet_upcase)
167
- pkt.received_count += 1
168
175
  command = pkt.clone
169
176
 
170
177
  # Restore the command's buffer to a zeroed string of defined length
@@ -187,7 +187,7 @@ module OpenC3
187
187
  # build_cmd('TGT','CMD',{'PARAM1'=>val,'PARAM2'=>val})
188
188
  def build_cmd(*args, range_check: true, raw: false, scope: $openc3_scope, **kwargs)
189
189
  extract_string_kwargs_to_args(args, kwargs)
190
- $api_server.build_command(*args)
190
+ $api_server.build_cmd(*args)
191
191
  end
192
192
  # build_command is DEPRECATED
193
193
  alias build_command build_cmd
@@ -216,6 +216,20 @@ module OpenC3
216
216
  # NOOP
217
217
  end
218
218
 
219
+ # Note: Enterprise Only - Use this for first time setup of an offline access token
220
+ # so that users can run scripts. Not necessary if accessing APIs via the web
221
+ # frontend as it handles it automatically.
222
+ #
223
+ # Example:
224
+ # initialize_offline_access()
225
+ # script_run("INST/procedures/collect.rb")
226
+ #
227
+ def initialize_offline_access
228
+ auth = OpenC3KeycloakAuthentication.new(ENV['OPENC3_KEYCLOAK_URL'])
229
+ auth.token(include_bearer: true, openid_scope: 'openid%20offline_access')
230
+ set_offline_access(auth.refresh_token)
231
+ end
232
+
219
233
  ###########################################################################
220
234
  # END PUBLIC API
221
235
  ###########################################################################
@@ -75,14 +75,14 @@ module OpenC3
75
75
  end
76
76
 
77
77
  # Load the token from the environment
78
- def token(include_bearer: true)
78
+ def token(include_bearer: true, openid_scope: 'openid')
79
79
  @auth_mutex.synchronize do
80
80
  @log = [nil, nil]
81
81
  current_time = Time.now.to_i
82
82
  if @token.nil?
83
- _make_token(current_time)
83
+ _make_token(current_time, openid_scope: openid_scope)
84
84
  elsif @refresh_expires_at < current_time
85
- _make_token(current_time)
85
+ _make_token(current_time, openid_scope: openid_scope)
86
86
  elsif @expires_at < current_time
87
87
  _refresh_token(current_time)
88
88
  end
@@ -108,13 +108,13 @@ module OpenC3
108
108
  private
109
109
 
110
110
  # Make the token and save token to instance
111
- def _make_token(current_time)
111
+ def _make_token(current_time, openid_scope: 'openid')
112
112
  client_id = ENV['OPENC3_API_CLIENT'] || 'api'
113
113
  if ENV['OPENC3_API_USER'] and ENV['OPENC3_API_PASSWORD']
114
114
  # Username and password
115
115
  data = "username=#{ENV['OPENC3_API_USER']}&password=#{ENV['OPENC3_API_PASSWORD']}"
116
116
  data << "&client_id=#{client_id}"
117
- data << '&grant_type=password&scope=openid'
117
+ data << "&grant_type=password&scope=#{openid_scope}"
118
118
  headers = {
119
119
  'Content-Type' => 'application/x-www-form-urlencoded',
120
120
  'User-Agent' => "OpenC3KeycloakAuthorization / #{OPENC3_VERSION} (ruby/openc3/lib/utilities/authentication)",
@@ -402,17 +402,20 @@ module OpenC3
402
402
  nil
403
403
  end
404
404
 
405
- def self.local_target_files(scope:, path_matchers:, include_temp: false)
405
+ def self.local_target_files(scope:, path_matchers:, include_temp: false, target: nil)
406
406
  files = []
407
407
  local_catalog = build_local_catalog(scope: scope)
408
408
  local_catalog.each do |key, _size|
409
409
  split_key = key.split('/')
410
410
  # DEFAULT/targets_modified/__TEMP__/YYYY_MM_DD_HH_MM_SS_mmm_temp.rb
411
411
  # See target_file.rb TEMP_FOLDER
412
- if split_key[2] === '__TEMP__'
412
+ if split_key[2] == '__TEMP__'
413
413
  files << split_key[2..-1].join('/') if include_temp
414
414
  next
415
415
  end
416
+ if target and split_key[2] != target
417
+ next
418
+ end
416
419
  if path_matchers
417
420
  found = false
418
421
  path_matchers.each do |path|