diff --git a/docs.openc3.com/docs/guides/scripting-api.md b/docs.openc3.com/docs/guides/scripting-api.md index 1abe4625fc..2d516d851b 100644 --- a/docs.openc3.com/docs/guides/scripting-api.md +++ b/docs.openc3.com/docs/guides/scripting-api.md @@ -3847,6 +3847,7 @@ packets.sort(key=lambda p: int(p['time'])) id, packets = get_packets(id) packets.sort_by! { |p| p['time'].to_i } ``` + ::: Returns a two element array containing the updated id and an array of packet hashes/dictionaries. Each packet hash/dictionary contains the following keys: @@ -4942,6 +4943,52 @@ set_limits_set("DEFAULT") +### delete_limits_set + +Since 7.3.0 + +Deletes a limits set and removes it from all telemetry items. The DEFAULT limits set and the currently active limits set cannot be deleted. Use [set_limits_set](#set_limits_set) to change the active set before deleting it. Use [get_limits_sets](#get_limits_sets) to get the available limit set names. + + + + +```python +delete_limits_set("") +``` + + + + + +```ruby +delete_limits_set("") +``` + + + + +| Parameter | Description | +| --------------- | --------------------------------- | +| Limits Set Name | Name of the limits set to delete. | + + + + +```python +delete_limits_set("TVAC") +``` + + + + + +```ruby +delete_limits_set("TVAC") +``` + + + + ### get_limits_set Since 5.0.0 diff --git a/openc3-cosmos-init/plugins/packages/openc3-js-common/src/services/openc3Api.js b/openc3-cosmos-init/plugins/packages/openc3-js-common/src/services/openc3Api.js index 10fdab08ec..b55d1fb411 100644 --- a/openc3-cosmos-init/plugins/packages/openc3-js-common/src/services/openc3Api.js +++ b/openc3-cosmos-init/plugins/packages/openc3-js-common/src/services/openc3Api.js @@ -364,6 +364,10 @@ export default class OpenC3Api { return this.exec('set_limits_set', [limits_set]) } + delete_limits_set(limits_set) { + return this.exec('delete_limits_set', [limits_set]) + } + // *********************************************** // End CmdTlmServer APIs // *********************************************** diff --git a/openc3/lib/openc3/api/limits_api.rb b/openc3/lib/openc3/api/limits_api.rb index 9300cb7d76..f2d4697c41 100644 --- a/openc3/lib/openc3/api/limits_api.rb +++ b/openc3/lib/openc3/api/limits_api.rb @@ -35,6 +35,7 @@ module Api 'get_limits_sets', 'set_limits_set', 'get_limits_set', + 'delete_limits_set', 'get_limits_events', ]) @@ -292,6 +293,43 @@ def set_limits_set(limits_set, manual: false, scope: $openc3_scope, token: $open time_nsec: Time.now.to_nsec_from_epoch, message: message }, scope: scope) end + # Deletes a limits set and removes it from all telemetry items. The DEFAULT + # limits set and the currently active limits set cannot be deleted. Use + # set_limits_set to change the active set before deleting it. + # + # @param limits_set [String] The name of the limits set to delete + def delete_limits_set(limits_set, manual: false, scope: $openc3_scope, token: $openc3_token) + authorize(permission: 'tlm_set', manual: manual, scope: scope, token: token) + limits_set = limits_set.to_s + raise "Cannot delete the DEFAULT limits set" if limits_set == 'DEFAULT' + if limits_set == LimitsEventTopic.current_set(scope: scope) + raise "Cannot delete the current limits set '#{limits_set}'. Use set_limits_set to change the current set first." + end + unless LimitsEventTopic.sets(scope: scope).key?(limits_set) + raise "Limits set '#{limits_set}' does not exist" + end + + # Remove the limits set from every telemetry item definition + TargetModel.names(scope: scope).each do |target_name| + TargetModel.packets(target_name, type: :TLM, scope: scope).each do |packet| + modified = false + packet['items'].each do |item| + if item['limits'] && item['limits'].key?(limits_set) + item['limits'].delete(limits_set) + modified = true + end + end + TargetModel.set_packet(target_name, packet['packet_name'], packet, scope: scope) if modified + end + end + + # Remove the limits set from Redis (limits_sets and current_limits_settings) + LimitsEventTopic.delete_set(limits_set, scope: scope) + + message = "Deleting Limits Set: #{limits_set}" + Logger.info(message, scope: scope) + end + # Returns the active limits set that applies to all telemetry # # @return [String] The current limits set diff --git a/openc3/lib/openc3/script/limits.rb b/openc3/lib/openc3/script/limits.rb index 929e07409f..cba06e007c 100644 --- a/openc3/lib/openc3/script/limits.rb +++ b/openc3/lib/openc3/script/limits.rb @@ -20,7 +20,7 @@ module Script private # Define all the modification methods such that we can disconnect them - %i(enable_limits disable_limits set_limits enable_limits_group disable_limits_group set_limits_set).each do |method_name| + %i(enable_limits disable_limits set_limits enable_limits_group disable_limits_group set_limits_set delete_limits_set).each do |method_name| define_method(method_name) do |*args, **kw_args, &block| kw_args[:scope] = $openc3_scope unless kw_args[:scope] if $disconnect diff --git a/openc3/lib/openc3/topics/limits_event_topic.rb b/openc3/lib/openc3/topics/limits_event_topic.rb index 109cdfb247..ca8a376f5f 100644 --- a/openc3/lib/openc3/topics/limits_event_topic.rb +++ b/openc3/lib/openc3/topics/limits_event_topic.rb @@ -173,6 +173,28 @@ def self.delete(target_name, packet_name = nil, scope:) end end + # Removes a limits set from the limits_sets hash and from the + # current_limits_settings of every item. Note that the items themselves + # (in the TargetModel packet definitions) are cleaned up in the + # delete_limits_set API. Running microservices will continue to hold the + # set in memory until they restart and resync from current_limits_settings. + def self.delete_set(set_name, scope:) + set_name = set_name.to_s + limits_settings = Store.hgetall("#{scope}__current_limits_settings") + # Collect all changed items and write them back in a single hmset to + # avoid a Redis round trip per item (this hash can be large) + updates = {} + limits_settings.each do |item, settings| + settings = JSON.parse(settings, allow_nan: true, create_additions: true) + if settings.key?(set_name) + settings.delete(set_name) + updates[item] = JSON.generate(settings, allow_nan: true) + end + end + Store.hmset("#{scope}__current_limits_settings", *updates.flatten) unless updates.empty? + Store.hdel("#{scope}__limits_sets", set_name) + end + # Update the local System based on overall state def self.sync_system(scope:) all_limits_settings = Store.hgetall("#{scope}__current_limits_settings") diff --git a/openc3/python/openc3/api/limits_api.py b/openc3/python/openc3/api/limits_api.py index 7ed4d56b18..eaac1d26ea 100644 --- a/openc3/python/openc3/api/limits_api.py +++ b/openc3/python/openc3/api/limits_api.py @@ -37,6 +37,7 @@ "get_limits_sets", "set_limits_set", "get_limits_set", + "delete_limits_set", "get_limits_events", ] ) @@ -369,6 +370,41 @@ def set_limits_set(limits_set, scope=OPENC3_SCOPE): ) +# Deletes a limits set and removes it from all telemetry items. The DEFAULT +# limits set and the currently active limits set cannot be deleted. Use +# set_limits_set to change the active set before deleting it. +# +# @param limits_set [String] The name of the limits set to delete +def delete_limits_set(limits_set, scope=OPENC3_SCOPE): + authorize(permission="tlm_set", scope=scope) + limits_set = str(limits_set) + if limits_set == "DEFAULT": + raise RuntimeError("Cannot delete the DEFAULT limits set") + if limits_set == LimitsEventTopic.current_set(scope=scope): + raise RuntimeError( + f"Cannot delete the current limits set '{limits_set}'. Use set_limits_set to change the current set first." + ) + if limits_set not in LimitsEventTopic.sets(scope=scope): + raise RuntimeError(f"Limits set '{limits_set}' does not exist") + + # Remove the limits set from every telemetry item definition + for target_name in TargetModel.names(scope=scope): + for packet in TargetModel.packets(target_name, type="TLM", scope=scope): + modified = False + for item in packet["items"]: + if item["limits"] and limits_set in item["limits"]: + del item["limits"][limits_set] + modified = True + if modified: + TargetModel.set_packet(target_name, packet["packet_name"], packet, scope=scope) + + # Remove the limits set from Redis (limits_sets and current_limits_settings) + LimitsEventTopic.delete_set(limits_set, scope=scope) + + message = f"Deleting Limits Set: {limits_set}" + Logger.info(message, scope=scope) + + # Returns the active limits set that applies to all telemetry # # @return [String] The current limits set diff --git a/openc3/python/openc3/script/limits.py b/openc3/python/openc3/script/limits.py index 8fd5283b40..4a7c7e03c2 100644 --- a/openc3/python/openc3/script/limits.py +++ b/openc3/python/openc3/script/limits.py @@ -20,6 +20,7 @@ "enable_limits_group", "disable_limits_group", "set_limits_set", + "delete_limits_set", ] # Define all the modification methods such that we can disconnect them diff --git a/openc3/python/openc3/topics/limits_event_topic.py b/openc3/python/openc3/topics/limits_event_topic.py index cf38b59b23..4f6a1f2c79 100644 --- a/openc3/python/openc3/topics/limits_event_topic.py +++ b/openc3/python/openc3/topics/limits_event_topic.py @@ -194,6 +194,29 @@ def delete(cls, target_name, packet_name=None, scope=None): if re.match(rf"^{target_name}__", item): Store.hdel(f"{scope}__current_limits_settings", item) + # Removes a limits set from the limits_sets hash and from the + # current_limits_settings of every item. Note that the items themselves + # (in the TargetModel packet definitions) are cleaned up in the + # delete_limits_set API. Running microservices will continue to hold the + # set in memory until they restart and resync from current_limits_settings. + @classmethod + def delete_set(cls, set_name, scope): + set_name = str(set_name) + limits_settings = Store.hgetall(f"{scope}__current_limits_settings") + # decode the binary string keys to strings + limits_settings = {k.decode(): v for (k, v) in limits_settings.items()} + # Collect all changed items and write them back in a single hset to + # avoid a Redis round trip per item (this hash can be large) + updates = {} + for item, settings in limits_settings.items(): + settings = json.loads(settings) + if set_name in settings: + del settings[set_name] + updates[item] = json.dumps(settings) + if updates: + Store.hset(f"{scope}__current_limits_settings", mapping=updates) + Store.hdel(f"{scope}__limits_sets", set_name) + # Update the local System based on overall state @classmethod def sync_system(cls, scope): diff --git a/openc3/python/test/api/test_limits_api.py b/openc3/python/test/api/test_limits_api.py index a540fa008b..6d511cf53e 100644 --- a/openc3/python/test/api/test_limits_api.py +++ b/openc3/python/test/api/test_limits_api.py @@ -259,6 +259,36 @@ def test_gets_and_set_the_active_limits_set(self): set_limits_set("DEFAULT") self.assertEqual(get_limits_set(), "DEFAULT") + def test_delete_limits_set_complains_about_default(self): + with self.assertRaisesRegex(RuntimeError, "Cannot delete the DEFAULT limits set"): + delete_limits_set("DEFAULT") + + def test_delete_limits_set_complains_about_current_set(self): + set_limits_set("TVAC") + with self.assertRaisesRegex(RuntimeError, "Cannot delete the current limits set 'TVAC'"): + delete_limits_set("TVAC") + + def test_delete_limits_set_complains_about_non_existent_set(self): + with self.assertRaisesRegex(RuntimeError, "Limits set 'NOPE' does not exist"): + delete_limits_set("NOPE") + + def test_delete_limits_set_removes_set_from_all_items(self): + self.assertEqual(get_limits_sets(), ["DEFAULT", "TVAC"]) + self.assertIn("TVAC", get_limits("INST", "HEALTH_STATUS", "TEMP1").keys()) + + delete_limits_set("TVAC") + + self.assertEqual(get_limits_sets(), ["DEFAULT"]) + self.assertNotIn("TVAC", get_limits("INST", "HEALTH_STATUS", "TEMP1").keys()) + self.assertIn("DEFAULT", get_limits("INST", "HEALTH_STATUS", "TEMP1").keys()) + + def test_delete_limits_set_created_with_set_limits(self): + set_limits("INST", "HEALTH_STATUS", "TEMP1", 0.0, 10.0, 20.0, 30.0) # creates CUSTOM + self.assertIn("CUSTOM", get_limits_sets()) + delete_limits_set("CUSTOM") + self.assertNotIn("CUSTOM", get_limits_sets()) + self.assertNotIn("CUSTOM", get_limits("INST", "HEALTH_STATUS", "TEMP1").keys()) + def test_get_limits_events_returns_an_offset_and_limits_event_hash(self): # Load the events topic with two events ... only the last should be returned event = { diff --git a/openc3/spec/api/limits_api_spec.rb b/openc3/spec/api/limits_api_spec.rb index 47b6cba84b..2be59d7023 100644 --- a/openc3/spec/api/limits_api_spec.rb +++ b/openc3/spec/api/limits_api_spec.rb @@ -194,6 +194,40 @@ def with_decom_microservice end end + describe "delete_limits_set" do + it "complains about deleting the DEFAULT limits set" do + expect { @api.delete_limits_set("DEFAULT") }.to raise_error(RuntimeError, "Cannot delete the DEFAULT limits set") + end + + it "complains about deleting the current limits set" do + @api.set_limits_set("TVAC") + expect { @api.delete_limits_set("TVAC") }.to raise_error(RuntimeError, /Cannot delete the current limits set 'TVAC'/) + end + + it "complains about non-existent limits sets" do + expect { @api.delete_limits_set("NOPE") }.to raise_error(RuntimeError, "Limits set 'NOPE' does not exist") + end + + it "deletes a limits set and removes it from all items" do + expect(@api.get_limits_sets).to eql ['DEFAULT', 'TVAC'] + expect(@api.get_limits("INST", "HEALTH_STATUS", "TEMP1").keys).to include('TVAC') + + @api.delete_limits_set("TVAC") + + expect(@api.get_limits_sets).to eql ['DEFAULT'] + expect(@api.get_limits("INST", "HEALTH_STATUS", "TEMP1").keys).to_not include('TVAC') + expect(@api.get_limits("INST", "HEALTH_STATUS", "TEMP1").keys).to include('DEFAULT') + end + + it "deletes a limits set created with set_limits" do + @api.set_limits("INST", "HEALTH_STATUS", "TEMP1", 0.0, 10.0, 20.0, 30.0) # creates CUSTOM + expect(@api.get_limits_sets).to include('CUSTOM') + @api.delete_limits_set("CUSTOM") + expect(@api.get_limits_sets).to_not include('CUSTOM') + expect(@api.get_limits("INST", "HEALTH_STATUS", "TEMP1").keys).to_not include('CUSTOM') + end + end + describe "get_limits_events" do it "returns empty array with no events" do events = @api.get_limits_events() diff --git a/openc3/spec/script/limits_spec.rb b/openc3/spec/script/limits_spec.rb index 85446a20e6..c58660fc46 100644 --- a/openc3/spec/script/limits_spec.rb +++ b/openc3/spec/script/limits_spec.rb @@ -51,7 +51,7 @@ module OpenC3 get_limits_events() # These methods are simply logged in disconnect mode and don't go through - setters = %i(enable_limits disable_limits set_limits enable_limits_group disable_limits_group set_limits_set) + setters = %i(enable_limits disable_limits set_limits enable_limits_group disable_limits_group set_limits_set delete_limits_set) setters.each do |method_name| if state == 'connected' expect(@proxy).to receive(method_name)