From 69ec755a9dd6b674f666391b6f25aadcd6327a5f Mon Sep 17 00:00:00 2001 From: Jason Thomas Date: Tue, 27 Aug 2024 14:27:30 -0600 Subject: [PATCH 01/10] Add support for command string and username --- .../INST2/lib/example_limits_response.py | 2 +- .../src/tools/admin/tabs/PluginsTab.vue | 11 ++--- openc3/lib/openc3/api/cmd_api.rb | 37 +++++++++++----- .../microservices/interface_microservice.rb | 9 +++- .../lib/openc3/topics/command_decom_topic.rb | 1 + openc3/lib/openc3/utilities/authorization.rb | 1 + openc3/lib/openc3/utilities/local_mode.rb | 1 + openc3/python/openc3/api/cmd_api.py | 42 ++++++++++++------- .../microservices/interface_microservice.py | 9 +++- .../openc3/topics/command_decom_topic.py | 1 + openc3/python/openc3/utilities/local_mode.py | 1 + playwright/package.json | 2 +- playwright/tests/admin/plugins.spec.ts | 6 +++ 13 files changed, 87 insertions(+), 36 deletions(-) diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/lib/example_limits_response.py b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/lib/example_limits_response.py index b2c0f216dc..9decfbc35f 100644 --- a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/lib/example_limits_response.py +++ b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/lib/example_limits_response.py @@ -22,6 +22,6 @@ class ExampleLimitsResponse(LimitsResponse): def call(self, packet, item, old_limits_state): match item.limits.state: case "RED_HIGH": - cmd("<%= target_name %>", "COLLECT", {"TYPE": "NORMAL", "DURATION": 7}) + cmd("<%= target_name %>", "COLLECT", {"TYPE": "NORMAL", "DURATION": 8}) case "RED_LOW": cmd_no_hazardous_check("<%= target_name %>", "CLEAR") diff --git a/openc3-cosmos-init/plugins/packages/openc3-tool-common/src/tools/admin/tabs/PluginsTab.vue b/openc3-cosmos-init/plugins/packages/openc3-tool-common/src/tools/admin/tabs/PluginsTab.vue index ef7067fb2b..663f8e4dfd 100644 --- a/openc3-cosmos-init/plugins/packages/openc3-tool-common/src/tools/admin/tabs/PluginsTab.vue +++ b/openc3-cosmos-init/plugins/packages/openc3-tool-common/src/tools/admin/tabs/PluginsTab.vue @@ -281,6 +281,7 @@ export default { 'openc3-cosmos-tool-admin', 'openc3-cosmos-tool-bucketexplorer', 'openc3-cosmos-tool-cmdsender', + 'openc3-cosmos-tool-cmdhistory', // Enterprise only 'openc3-cosmos-tool-cmdtlmserver', 'openc3-cosmos-tool-dataextractor', 'openc3-cosmos-tool-dataviewer', @@ -293,11 +294,11 @@ export default { 'openc3-cosmos-tool-tablemanager', 'openc3-cosmos-tool-tlmgrapher', 'openc3-cosmos-tool-tlmviewer', - 'openc3-cosmos-enterprise-tool-admin', - 'openc3-cosmos-tool-autonomic', - 'openc3-cosmos-tool-calendar', - 'openc3-cosmos-tool-grafana', - 'openc3-enterprise-tool-base', + 'openc3-cosmos-enterprise-tool-admin', // Enterprise only + 'openc3-cosmos-tool-autonomic', // Enterprise only + 'openc3-cosmos-tool-calendar', // Enterprise only + 'openc3-cosmos-tool-grafana', // Enterprise only + 'openc3-enterprise-tool-base', // Enterprise only 'openc3-tool-base', ], } diff --git a/openc3/lib/openc3/api/cmd_api.rb b/openc3/lib/openc3/api/cmd_api.rb index 6e673c077c..38abe1c547 100644 --- a/openc3/lib/openc3/api/cmd_api.rb +++ b/openc3/lib/openc3/api/cmd_api.rb @@ -484,7 +484,19 @@ def _cmd_implementation(method_name, *args, range_check:, hazardous_check:, raw: target_name = target_name.upcase cmd_name = cmd_name.upcase cmd_params = cmd_params.transform_keys(&:upcase) - authorize(permission: 'cmd', target_name: target_name, packet_name: cmd_name, manual: manual, scope: scope, token: token) + user = authorize(permission: 'cmd', target_name: target_name, packet_name: cmd_name, manual: manual, scope: scope, token: token) + if user.nil? + caller.each do |frame| + # Look for the following line in the stack trace which indicates custom code + # /tmp/d20240827-62-8e57pf/targets/INST/lib/example_limits_response.rb:31:in `call' + if frame.include?("/targets/#{target_name}") + user = {} + # username is the name of the custom code file + user['username'] = frame.split("/targets/")[-1].split(':')[0] + break + end + end + end packet = TargetModel.packet(target_name, cmd_name, type: :CMD, scope: scope) if packet['disabled'] error = DisabledError.new @@ -493,14 +505,6 @@ def _cmd_implementation(method_name, *args, range_check:, hazardous_check:, raw: raise error end - command = { - 'target_name' => target_name, - 'cmd_name' => cmd_name, - 'cmd_params' => cmd_params, - 'range_check' => range_check.to_s, - 'hazardous_check' => hazardous_check.to_s, - 'raw' => raw.to_s - } if log_message.nil? # This means the default was used, no argument was passed log_message = true # Default is true # If the packet has the DISABLE_MESSAGES keyword then no messages by default @@ -513,9 +517,22 @@ def _cmd_implementation(method_name, *args, range_check:, hazardous_check:, raw: end end end + cmd_string = _build_cmd_output_string(method_name, target_name, cmd_name, cmd_params, packet) if log_message - Logger.info(_build_cmd_output_string(method_name, target_name, cmd_name, cmd_params, packet), scope: scope) + Logger.info(cmd_string, scope: scope) end + + username = user && user['username'] ? user['username'] : 'anonymous' + command = { + 'target_name' => target_name, + 'cmd_name' => cmd_name, + 'cmd_params' => cmd_params, + 'range_check' => range_check.to_s, + 'hazardous_check' => hazardous_check.to_s, + 'raw' => raw.to_s, + 'cmd_string' => cmd_string, + 'username' => username + } CommandTopic.send_command(command, timeout: timeout, scope: scope) end diff --git a/openc3/lib/openc3/microservices/interface_microservice.rb b/openc3/lib/openc3/microservices/interface_microservice.rb index db8fb830f8..f950b921ec 100644 --- a/openc3/lib/openc3/microservices/interface_microservice.rb +++ b/openc3/lib/openc3/microservices/interface_microservice.rb @@ -191,11 +191,14 @@ def run next e.message end + command.extra ||= {} + command.extra['cmd_string'] = msg_hash['cmd_string'] + command.extra['username'] = msg_hash['username'] if hazardous_check hazardous, hazardous_description = System.commands.cmd_pkt_hazardous?(command) # Return back the error, description, and the formatted command # This allows the error handler to simply re-send the command - next "HazardousError\n#{hazardous_description}\n#{System.commands.format(command)}" if hazardous + next "HazardousError\n#{hazardous_description}\n#{msg_hash['cmd_string']}" if hazardous end begin @@ -204,8 +207,10 @@ def run @metric.set(name: 'interface_cmd_total', value: @count, type: 'counter') if @metric @interface.write(command) - CommandTopic.write_packet(command, scope: @scope) CommandDecomTopic.write_packet(command, scope: @scope) + # Remove cmd_string from the raw packet logging + command.extra.delete('cmd_string') + CommandTopic.write_packet(command, scope: @scope) InterfaceStatusModel.set(@interface.as_json(:allow_nan => true), queued: true, scope: @scope) next 'SUCCESS' else diff --git a/openc3/lib/openc3/topics/command_decom_topic.rb b/openc3/lib/openc3/topics/command_decom_topic.rb index c7ccd2f91a..f7cf0ebe33 100644 --- a/openc3/lib/openc3/topics/command_decom_topic.rb +++ b/openc3/lib/openc3/topics/command_decom_topic.rb @@ -39,6 +39,7 @@ def self.write_packet(packet, scope:) json_hash[item.name + "__F"] = packet.read_item(item, :FORMATTED) if item.format_string json_hash[item.name + "__U"] = packet.read_item(item, :WITH_UNITS) if item.units end + json_hash['extra'] = JSON.generate(packet.extra.as_json(:allow_nan => true)) msg_hash['json_data'] = JSON.generate(json_hash.as_json(:allow_nan => true)) EphemeralStoreQueued.write_topic(topic, msg_hash) end diff --git a/openc3/lib/openc3/utilities/authorization.rb b/openc3/lib/openc3/utilities/authorization.rb index a95c345a17..6a5b581582 100644 --- a/openc3/lib/openc3/utilities/authorization.rb +++ b/openc3/lib/openc3/utilities/authorization.rb @@ -46,6 +46,7 @@ def authorize(permission: nil, target_name: nil, packet_name: nil, interface_nam raise AuthError.new("Password is invalid for '#{permission}' permission") end end + return "anonymous" end def user_info(_token) diff --git a/openc3/lib/openc3/utilities/local_mode.rb b/openc3/lib/openc3/utilities/local_mode.rb index 016bb156bc..76022994fe 100644 --- a/openc3/lib/openc3/utilities/local_mode.rb +++ b/openc3/lib/openc3/utilities/local_mode.rb @@ -31,6 +31,7 @@ module LocalMode 'openc3-cosmos-tool-admin', 'openc3-cosmos-tool-bucketexplorer', 'openc3-cosmos-tool-cmdsender', + 'openc3-cosmos-tool-cmdhistory', 'openc3-cosmos-tool-cmdtlmserver', 'openc3-cosmos-tool-dataextractor', 'openc3-cosmos-tool-dataviewer', diff --git a/openc3/python/openc3/api/cmd_api.py b/openc3/python/openc3/api/cmd_api.py index 405ed7299a..1c8b44393e 100644 --- a/openc3/python/openc3/api/cmd_api.py +++ b/openc3/python/openc3/api/cmd_api.py @@ -14,6 +14,7 @@ # This file may also be used under the terms of a commercial license # if purchased from OpenC3, Inc. +import traceback from contextlib import contextmanager from openc3.api import WHITELIST from openc3.api.interface_api import get_interface @@ -29,7 +30,6 @@ from openc3.topics.interface_topic import InterfaceTopic from openc3.topics.decom_interface_topic import DecomInterfaceTopic from openc3.topics.command_decom_topic import CommandDecomTopic - from openc3.packets.packet import Packet WHITELIST.extend( @@ -582,7 +582,18 @@ def _cmd_implementation( target_name = target_name.upper() cmd_name = cmd_name.upper() cmd_params = {k.upper(): v for k, v in cmd_params.items()} - authorize(permission="cmd", target_name=target_name, packet_name=cmd_name, scope=scope) + user = authorize(permission="cmd", target_name=target_name, packet_name=cmd_name, scope=scope) + if not user: + stack_trace = traceback.extract_stack() + for frame in stack_trace: + # Look for the following line in the stack trace which indicates custom code + # File "/tmp/tmpp96e6j83/targets/INST2/lib/example_limits_response.py", line 25, in call" + if f"/targets/{target_name}" in frame.filename: + user = {} + # username is the name of the custom code file + user["username"] = frame.filename.split("/targets/")[-1].split('"')[0] + break + packet = TargetModel.packet(target_name, cmd_name, type="CMD", scope=scope) if packet.get("disabled", False): error = DisabledError() @@ -590,15 +601,6 @@ def _cmd_implementation( error.cmd_name = cmd_name raise error - command = { - "target_name": target_name, - "cmd_name": cmd_name, - "cmd_params": cmd_params, - "range_check": str(range_check), - "hazardous_check": str(hazardous_check), - "raw": str(raw), - } - timeout = None if kwargs.get("timeout") is not None: try: @@ -631,11 +633,21 @@ def _cmd_implementation( if kwargs["log_message"] not in [True, False]: raise RuntimeError(f"Invalid log_message parameter: {log_message}. Must be True or False.") log_message = kwargs["log_message"] + cmd_string = _cmd_log_string(method_name, target_name, cmd_name, cmd_params, packet) if log_message: - Logger.info( - _cmd_log_string(method_name, target_name, cmd_name, cmd_params, packet), - scope, - ) + Logger.info(cmd_string, scope) + + username = user["username"] if user and user["username"] else "anonymous" + command = { + "target_name": target_name, + "cmd_name": cmd_name, + "cmd_params": cmd_params, + "range_check": str(range_check), + "hazardous_check": str(hazardous_check), + "raw": str(raw), + "cmd_stirng": cmd_string, + "username": username, + } return CommandTopic.send_command(command, timeout, scope) diff --git a/openc3/python/openc3/microservices/interface_microservice.py b/openc3/python/openc3/microservices/interface_microservice.py index aee2b884cc..e0cb3567a5 100644 --- a/openc3/python/openc3/microservices/interface_microservice.py +++ b/openc3/python/openc3/microservices/interface_microservice.py @@ -208,12 +208,15 @@ def process_cmd(self, topic, msg_id, msg_hash, redis): self.logger.error(f"{self.interface.name}: {repr(error)}") return repr(error) + command.extra = {} or command.extra + command.extra["cmd_string"] = msg_hash["cmd_string"] + command.extra["username"] = msg_hash["username"] if hazardous_check: hazardous, hazardous_description = System.commands.cmd_pkt_hazardous(command) # Return back the error, description, and the formatted command # This allows the error handler to simply re-send the command if hazardous: - return f"HazardousError\n{hazardous_description}\n{System.commands.format(command)}" + return f"HazardousError\n{hazardous_description}\n{msg_hash["cmd_string"]}" try: if self.interface.connected(): @@ -222,8 +225,10 @@ def process_cmd(self, topic, msg_id, msg_hash, redis): self.metric.set(name="interface_cmd_total", value=self.count, type="counter") self.interface.write(command) - CommandTopic.write_packet(command, scope=self.scope) CommandDecomTopic.write_packet(command, scope=self.scope) + # Remove cmd_string from the raw packet logging + command.extra.pop("cmd_string") + CommandTopic.write_packet(command, scope=self.scope) InterfaceStatusModel.set(self.interface.as_json(), queued=True, scope=self.scope) return "SUCCESS" else: diff --git a/openc3/python/openc3/topics/command_decom_topic.py b/openc3/python/openc3/topics/command_decom_topic.py index 5fa8cd8685..6465e3cb99 100644 --- a/openc3/python/openc3/topics/command_decom_topic.py +++ b/openc3/python/openc3/topics/command_decom_topic.py @@ -46,6 +46,7 @@ def write_packet(cls, packet, scope): json_hash[item.name + "__F"] = packet.read_item(item, "FORMATTED") if item.units: json_hash[item.name + "__U"] = packet.read_item(item, "WITH_UNITS") + json_hash["extra"] = json.dumps(packet.extra, cls=JsonEncoder) msg_hash["json_data"] = json.dumps(json_hash, cls=JsonEncoder) EphemeralStoreQueued.write_topic(topic, msg_hash) diff --git a/openc3/python/openc3/utilities/local_mode.py b/openc3/python/openc3/utilities/local_mode.py index 4046d8b51f..1100f002a9 100644 --- a/openc3/python/openc3/utilities/local_mode.py +++ b/openc3/python/openc3/utilities/local_mode.py @@ -26,6 +26,7 @@ class LocalMode: "openc3-cosmos-tool-admin", "openc3-cosmos-tool-bucketexplorer", "openc3-cosmos-tool-cmdsender", + "openc3-cosmos-tool-cmdhistory", "openc3-cosmos-tool-cmdtlmserver", "openc3-cosmos-tool-dataextractor", "openc3-cosmos-tool-dataviewer", diff --git a/playwright/package.json b/playwright/package.json index 65b4798e9d..d551320452 100644 --- a/playwright/package.json +++ b/playwright/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@playwright/test": "1.46.0", - "date-fns": "^3.2.0", + "date-fns": "3.6.0", "jszip": "^3.10.1", "nyc": "^17.0.0", "prettier": "^3.1.1" diff --git a/playwright/tests/admin/plugins.spec.ts b/playwright/tests/admin/plugins.spec.ts index 9d6fcd48ab..9298f7200c 100644 --- a/playwright/tests/admin/plugins.spec.ts +++ b/playwright/tests/admin/plugins.spec.ts @@ -40,6 +40,9 @@ test('shows and hides built-in tools', async ({ page, utils }) => { await expect(page.locator('id=openc3-tool')).not.toContainText( 'openc3-cosmos-tool-calendar', ) + await expect(page.locator('id=openc3-tool')).not.toContainText( + 'openc3-cosmos-tool-cmdhistory', + ) await expect(page.locator('id=openc3-tool')).not.toContainText( 'openc3-cosmos-tool-grafana', ) @@ -108,6 +111,9 @@ test('shows and hides built-in tools', async ({ page, utils }) => { await expect(page.locator('id=openc3-tool')).toContainText( 'openc3-cosmos-tool-calendar', ) + await expect(page.locator('id=openc3-tool')).toContainText( + 'openc3-cosmos-tool-cmdhistory', + ) await expect(page.locator('id=openc3-tool')).toContainText( 'openc3-cosmos-tool-grafana', ) From 574425336fa45bdcbafafcd92911e88bd1333b5a Mon Sep 17 00:00:00 2001 From: Jason Thomas Date: Thu, 29 Aug 2024 15:56:12 -0600 Subject: [PATCH 02/10] Implement validator --- .../packages/openc3-cosmos-demo/plugin.txt | 1 - .../targets/EXAMPLE/lib/my_reject_protocol.rb | 21 ---- .../targets/INST/cmd_tlm/inst_cmds.txt | 13 +++ .../targets/INST/cmd_tlm/inst_tlm.txt | 1 + .../targets/INST/lib/cmd_validator.rb | 37 ++++++ .../INST/lib/example_limits_response.rb | 6 +- .../targets/INST/lib/sim_inst.rb | 67 ++++++----- .../targets/INST2/cmd_tlm/inst_cmds.txt | 18 +++ .../targets/INST2/cmd_tlm/inst_tlm.txt | 18 ++- .../targets/INST2/lib/cmd_validator.py | 30 +++++ .../INST2/lib/example_limits_response.py | 13 ++- .../targets/INST2/lib/sim_inst.py | 34 ++++-- openc3/data/config/command_modifiers.yaml | 52 +++++++++ openc3/lib/openc3/api/cmd_api.rb | 30 +++-- openc3/lib/openc3/core_ext/exception.rb | 11 +- .../microservices/decom_microservice.rb | 32 ++++-- .../microservices/interface_microservice.rb | 36 +++++- openc3/lib/openc3/packets/packet.rb | 4 + openc3/lib/openc3/packets/packet_config.rb | 15 +-- openc3/lib/openc3/script/api_shared.rb | 3 +- openc3/lib/openc3/topics/command_topic.rb | 6 +- .../openc3/topics/decom_interface_topic.rb | 2 +- openc3/python/openc3/api/cmd_api.py | 76 +++++++------ openc3/python/openc3/interfaces/interface.py | 6 +- .../microservices/decom_microservice.py | 22 ++-- .../microservices/interface_microservice.py | 62 +++++++---- openc3/python/openc3/packets/packet.py | 1 + openc3/python/openc3/packets/packet_config.py | 18 ++- openc3/python/openc3/topics/command_topic.py | 4 +- .../openc3/topics/decom_interface_topic.py | 2 +- openc3/python/openc3/utilities/local_mode.py | 2 +- openc3/python/test/api/test_cmd_api.py | 105 +++++++++--------- openc3/spec/api/cmd_api_spec.rb | 4 +- openc3/spec/core_ext/exception_spec.rb | 62 ++++++----- .../interface_microservice_spec.rb | 25 +++++ 35 files changed, 573 insertions(+), 266 deletions(-) delete mode 100644 openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/EXAMPLE/lib/my_reject_protocol.rb create mode 100644 openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST/lib/cmd_validator.rb create mode 100644 openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/lib/cmd_validator.py diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/plugin.txt b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/plugin.txt index 6769b27936..bf95651172 100644 --- a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/plugin.txt +++ b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/plugin.txt @@ -91,7 +91,6 @@ VARIABLE reduced_log_retain_time 2592000 # This expression builds the correct hostname for Open Source or Enterprise Edition in Kubernetes <% example_host = ENV['KUBERNETES_SERVICE_HOST'] ? "#{scope}-user-#{example_microservice_name.downcase.gsub('__', '-').gsub('_', '-')}-service" : "openc3-operator" %> INTERFACE <%= example_int_name %> example_interface.rb <%= example_host %> <%= example_port %> - PROTOCOL WRITE MyRejectProtocol MAP_TARGET <%= example_target_name %> DONT_CONNECT # Override the default log time of 600 diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/EXAMPLE/lib/my_reject_protocol.rb b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/EXAMPLE/lib/my_reject_protocol.rb deleted file mode 100644 index 8ec85453e1..0000000000 --- a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/EXAMPLE/lib/my_reject_protocol.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'openc3/interfaces/interface' -require 'openc3/interfaces/protocols/protocol' -require 'openc3/api/api' - -module OpenC3 - class MyRejectProtocol < Protocol - include Api - - def write_packet(packet) - if packet.packet_name == 'START' - temp = tlm("INST HEALTH_STATUS TEMP1") - if temp > 50 - raise WriteRejectError, "TEMP1 too high for command" - elsif temp < -50 - raise WriteRejectError, "TEMP1 too low for command" - end - end - return packet - end - end -end diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST/cmd_tlm/inst_cmds.txt b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST/cmd_tlm/inst_cmds.txt index 8cfbb9b27f..b7abfa18c5 100644 --- a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST/cmd_tlm/inst_cmds.txt +++ b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST/cmd_tlm/inst_cmds.txt @@ -1,4 +1,5 @@ COMMAND <%= target_name %> COLLECT BIG_ENDIAN "Starts a collect on the <%= target_name %> target" + VALIDATOR cmd_validator.rb <%= render "_ccsds_cmd.txt", locals: {id: 1} %> PARAMETER TYPE 64 16 UINT MIN MAX 0 "Collect type which can be normal or special. Note the special collects are hazarous and require user confirmation." REQUIRED @@ -14,14 +15,17 @@ COMMAND <%= target_name %> COLLECT BIG_ENDIAN "Starts a collect on the <%= targe RELATED_ITEM <%= target_name %> HEALTH_STATUS COLLECT_TYPE COMMAND <%= target_name %> ABORT BIG_ENDIAN "Aborts a collect on the <%= target_name %> instrument" + VALIDATOR cmd_validator.rb <%= render "_ccsds_cmd.txt", locals: {id: 2} %> COMMAND <%= target_name %> CLEAR BIG_ENDIAN "Clears counters on the <%= target_name %> instrument" + VALIDATOR cmd_validator.rb HAZARDOUS "Clearing counters may lose valuable information." <%= render "_ccsds_cmd.txt", locals: {id: 3} %> RELATED_ITEM <%= target_name %> HEALTH_STATUS COLLECTS COMMAND <%= target_name %> SETPARAMS BIG_ENDIAN "Sets numbered parameters" + VALIDATOR cmd_validator.rb <%= render "_ccsds_cmd.txt", locals: {id: 4} %> # ERB syntax: <% (1..5).each do |i| %> @@ -35,6 +39,7 @@ COMMAND <%= target_name %> SETPARAMS BIG_ENDIAN "Sets numbered parameters" POLY_WRITE_CONVERSION 0 2 COMMAND <%= target_name %> ASCIICMD BIG_ENDIAN "Enumerated ASCII command" + VALIDATOR cmd_validator.rb <%= render "_ccsds_cmd.txt", locals: {id: 5} %> APPEND_PARAMETER STRING 2048 STRING "NOOP" "Enumerated string parameter" STATE "ARM LASER" "ARM LASER" HAZARDOUS "Arming the laser poses an eye safety hazard." @@ -45,16 +50,19 @@ COMMAND <%= target_name %> ASCIICMD BIG_ENDIAN "Enumerated ASCII command" RELATED_ITEM <%= target_name %> HEALTH_STATUS ASCIICMD COMMAND <%= target_name %> FLTCMD BIG_ENDIAN "Command with float parameters" + VALIDATOR cmd_validator.rb <%= render "_ccsds_cmd.txt", locals: {id: 6} %> PARAMETER FLOAT32 64 32 FLOAT MIN MAX 0.0 "Float32 parameter" PARAMETER FLOAT64 96 64 FLOAT MIN MAX 0.0 "Float64 parameter" COMMAND <%= target_name %> ARYCMD BIG_ENDIAN "Command with array parameter" + VALIDATOR cmd_validator.rb <%= render "_ccsds_cmd.txt", locals: {id: 7} %> ARRAY_PARAMETER ARRAY 64 32 UINT -8 "Array parameter" PARAMETER CRC -8 8 UINT MIN MAX 0 "CRC" COMMAND <%= target_name %> SLRPNLDEPLOY BIG_ENDIAN "Deploy solar array panels" + VALIDATOR cmd_validator.rb <%= render "_ccsds_cmd.txt", locals: {id: 8} %> RELATED_ITEM <%= target_name %> MECH SLRPNL1 RELATED_ITEM <%= target_name %> MECH SLRPNL2 @@ -63,6 +71,7 @@ COMMAND <%= target_name %> SLRPNLDEPLOY BIG_ENDIAN "Deploy solar array panels" RELATED_ITEM <%= target_name %> MECH SLRPNL5 COMMAND <%= target_name %> SLRPNLRESET BIG_ENDIAN "Reset solar array panels" + VALIDATOR cmd_validator.rb <%= render "_ccsds_cmd.txt", locals: {id: 9} %> RELATED_ITEM <%= target_name %> MECH SLRPNL1 RELATED_ITEM <%= target_name %> MECH SLRPNL2 @@ -71,22 +80,26 @@ COMMAND <%= target_name %> SLRPNLRESET BIG_ENDIAN "Reset solar array panels" RELATED_ITEM <%= target_name %> MECH SLRPNL5 COMMAND <%= target_name %> MEMLOAD BIG_ENDIAN "Load memory" + VALIDATOR cmd_validator.rb DISABLE_MESSAGES # Disable messages on a command that could be sent many many times <%= render "_ccsds_cmd.txt", locals: {id: 10} %> APPEND_PARAMETER DATA 80 BLOCK "" "Block of data" RELATED_ITEM <%= target_name %> HEALTH_STATUS BLOCKTEST COMMAND <%= target_name %> QUIET BIG_ENDIAN "Enable/disable no out of limits in the demo" + VALIDATOR cmd_validator.rb <%= render "_ccsds_cmd.txt", locals: {id: 11} %> APPEND_PARAMETER STATE 8 UINT 0 1 1 STATE FALSE 0 STATE TRUE 1 COMMAND <%= target_name %> TIME_OFFSET BIG_ENDIAN "Subtract the packet time by the given seconds" + VALIDATOR cmd_validator.rb <%= render "_ccsds_cmd.txt", locals: {id: 12} %> APPEND_PARAMETER SECONDS 32 UINT MIN MAX 0 "Seconds to subtract from packet time" COMMAND <%= target_name %> HIDDEN BIG_ENDIAN "Hidden command to bump the hidden packet" + VALIDATOR cmd_validator.rb HIDDEN <%= render "_ccsds_cmd.txt", locals: {id: 13} %> APPEND_PARAMETER COUNT 32 UINT MIN MAX 0 "Count to set" diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST/cmd_tlm/inst_tlm.txt b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST/cmd_tlm/inst_tlm.txt index 281432c8d9..8d6ca50643 100644 --- a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST/cmd_tlm/inst_tlm.txt +++ b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST/cmd_tlm/inst_tlm.txt @@ -1,5 +1,6 @@ TELEMETRY <%= target_name %> HEALTH_STATUS BIG_ENDIAN "Health and status from the <%= target_name %> target" <%= render "_ccsds_tlm.txt", locals: {apid: 1} %> + APPEND_ITEM CMD_ACPT_CNT 32 UINT "Command accept count" APPEND_ITEM COLLECTS 16 UINT "Number of collects" APPEND_ITEM TEMP1 16 UINT "Temperature #1" POLY_READ_CONVERSION -100.0 0.00305 diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST/lib/cmd_validator.rb b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST/lib/cmd_validator.rb new file mode 100644 index 0000000000..2fcb9bc3b1 --- /dev/null +++ b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST/lib/cmd_validator.rb @@ -0,0 +1,37 @@ +# encoding: ascii-8bit + +# Copyright 2024 OpenC3, Inc. +# All Rights Reserved. +# +# This program is free software; you can modify and/or redistribute it +# under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation; version 3 with +# attribution addendums as found in the LICENSE.txt +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# This file may also be used under the terms of a commercial license +# if purchased from OpenC3, Inc. + +require 'openc3/packets/command_validator' + +class CmdValidator < OpenC3::CommandValidator + def pre_check(command) + @cmd_acpt_cnt = tlm("INST HEALTH_STATUS CMD_ACPT_CNT") + puts "pre_check: #{@cmd_acpt_cnt}" + return [true, nil] + end + + def post_check(command) + puts "post_check: #{@cmd_acpt_cnt}" + if command.packet_name == 'CLEAR' + wait_check("INST HEALTH_STATUS CMD_ACPT_CNT == 0", 10) + else + wait_check("INST HEALTH_STATUS CMD_ACPT_CNT > #{@cmd_acpt_cnt}", 10) + end + return [true, nil] + end +end diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST/lib/example_limits_response.rb b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST/lib/example_limits_response.rb index 37a7216198..5c9bc7418e 100644 --- a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST/lib/example_limits_response.rb +++ b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST/lib/example_limits_response.rb @@ -14,7 +14,7 @@ # GNU Affero General Public License for more details. # Modified by OpenC3, Inc. -# All changes Copyright 2023, OpenC3, Inc. +# All changes Copyright 2024, OpenC3, Inc. # All Rights Reserved # # This file may also be used under the terms of a commercial license @@ -28,9 +28,9 @@ class ExampleLimitsResponse < OpenC3::LimitsResponse def call(packet, item, old_limits_state) case item.limits.state when :RED_HIGH - cmd('<%= target_name %>', 'COLLECT', 'TYPE' => 'NORMAL', 'DURATION' => 7) + cmd('<%= target_name %> COLLECT with TYPE NORMAL, DURATION 7', validator: false) when :RED_LOW - cmd_no_hazardous_check('<%= target_name %>', 'CLEAR') + cmd('<%= target_name %> ABORT', validator: false) end end end diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST/lib/sim_inst.rb b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST/lib/sim_inst.rb index 07f7137f5c..6e4c8d693d 100644 --- a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST/lib/sim_inst.rb +++ b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST/lib/sim_inst.rb @@ -14,7 +14,7 @@ # GNU Affero General Public License for more details. # Modified by OpenC3, Inc. -# All changes Copyright 2022, OpenC3, Inc. +# All changes Copyright 2024, OpenC3, Inc. # All Rights Reserved # # This file may also be used under the terms of a commercial license @@ -131,17 +131,17 @@ def initialize(target_name) @solar_panel_thread = nil @solar_panel_thread_cancel = false - @trackStars = Array.new - @trackStars[0] = 1237 - @trackStars[1] = 1329 - @trackStars[2] = 1333 - @trackStars[3] = 1139 - @trackStars[4] = 1161 - @trackStars[5] = 682 - @trackStars[6] = 717 - @trackStars[7] = 814 - @trackStars[8] = 583 - @trackStars[9] = 622 + @track_stars = Array.new + @track_stars[0] = 1237 + @track_stars[1] = 1329 + @track_stars[2] = 1333 + @track_stars[3] = 1139 + @track_stars[4] = 1161 + @track_stars[5] = 682 + @track_stars[6] = 717 + @track_stars[7] = 814 + @track_stars[8] = 583 + @track_stars[9] = 622 @bad_temp2 = false @last_temp2 = 0 @@ -174,31 +174,27 @@ def write(packet) case name when 'COLLECT' + hs_packet.cmd_acpt_cnt += 1 hs_packet.collects += 1 hs_packet.duration = packet.read('duration') hs_packet.collect_type = packet.read("type") + when 'ABORT', 'FLTCMD', 'ARYCMD' + hs_packet.cmd_acpt_cnt += 1 when 'CLEAR' + hs_packet.cmd_acpt_cnt = 0 hs_packet.collects = 0 - when 'MEMLOAD' - hs_packet.blocktest = packet.read('data') - when 'QUIET' - if packet.read('state') == 'TRUE' - @quiet = true - else - @quiet = false - end - when 'TIME_OFFSET' - @time_offset = packet.read('seconds') when 'SETPARAMS' - # puts "SETPARAMS packet: #{packet.buffer.formatted}" + hs_packet.cmd_acpt_cnt += 1 params_packet.value1 = packet.read('value1') params_packet.value2 = packet.read('value2') params_packet.value3 = packet.read('value3') params_packet.value4 = packet.read('value4') params_packet.value5 = packet.read('value5') when 'ASCIICMD' + hs_packet.cmd_acpt_cnt += 1 hs_packet.asciicmd = packet.read('string') when 'SLRPNLDEPLOY' + hs_packet.cmd_acpt_cnt += 1 return if @solar_panel_thread and @solar_panel_thread.alive? @solar_panel_thread = Thread.new do @solar_panel_thread_cancel = false @@ -221,9 +217,24 @@ def write(packet) end end when 'SLRPNLRESET' + hs_packet.cmd_acpt_cnt += 1 OpenC3.kill_thread(self, @solar_panel_thread) @solar_panel_positions = SOLAR_PANEL_DFLTS.dup + when 'MEMLOAD' + hs_packet.cmd_acpt_cnt += 1 + hs_packet.blocktest = packet.read('data') + when 'QUIET' + hs_packet.cmd_acpt_cnt += 1 + if packet.read('state') == 'TRUE' + @quiet = true + else + @quiet = false + end + when 'TIME_OFFSET' + hs_packet.cmd_acpt_cnt += 1 + @time_offset = packet.read('seconds') when 'HIDDEN' + # Deliberately do not increment cmd_acpt_cnt @tlm_packets['HIDDEN'].count = packet.read('count') end end @@ -286,11 +297,11 @@ def read(count_100hz, time) packet.biasy = @att_packet.biasy packet.biasy = @att_packet.biasz - packet.star1id = @trackStars[((count_100hz / 100) + 0) % 10] - packet.star2id = @trackStars[((count_100hz / 100) + 1) % 10] - packet.star3id = @trackStars[((count_100hz / 100) + 2) % 10] - packet.star4id = @trackStars[((count_100hz / 100) + 3) % 10] - packet.star5id = @trackStars[((count_100hz / 100) + 4) % 10] + packet.star1id = @track_stars[((count_100hz / 100) + 0) % 10] + packet.star2id = @track_stars[((count_100hz / 100) + 1) % 10] + packet.star3id = @track_stars[((count_100hz / 100) + 2) % 10] + packet.star4id = @track_stars[((count_100hz / 100) + 3) % 10] + packet.star5id = @track_stars[((count_100hz / 100) + 4) % 10] packet.posprogress = (@position_file_bytes_read.to_f / @position_file_size.to_f) * 100.0 packet.attprogress = (@attitude_file_bytes_read.to_f / @attitude_file_size.to_f) * 100.0 diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/cmd_tlm/inst_cmds.txt b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/cmd_tlm/inst_cmds.txt index 3baa96ee04..0d058b2505 100644 --- a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/cmd_tlm/inst_cmds.txt +++ b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/cmd_tlm/inst_cmds.txt @@ -1,4 +1,5 @@ COMMAND <%= target_name %> COLLECT BIG_ENDIAN "Starts a collect on the <%= target_name %> target" + VALIDATOR cmd_validator.py <%= render "_ccsds_cmd.txt", locals: {id: 1} %> PARAMETER TYPE 64 16 UINT MIN MAX 0 "Collect type which can be normal or special. Note the special collects are hazarous and require user confirmation." REQUIRED @@ -14,14 +15,17 @@ COMMAND <%= target_name %> COLLECT BIG_ENDIAN "Starts a collect on the <%= targe RELATED_ITEM <%= target_name %> HEALTH_STATUS COLLECT_TYPE COMMAND <%= target_name %> ABORT BIG_ENDIAN "Aborts a collect on the <%= target_name %> instrument" + VALIDATOR cmd_validator.py <%= render "_ccsds_cmd.txt", locals: {id: 2} %> COMMAND <%= target_name %> CLEAR BIG_ENDIAN "Clears counters on the <%= target_name %> instrument" + VALIDATOR cmd_validator.py HAZARDOUS "Clearing counters may lose valuable information." <%= render "_ccsds_cmd.txt", locals: {id: 3} %> RELATED_ITEM <%= target_name %> HEALTH_STATUS COLLECTS COMMAND <%= target_name %> SETPARAMS BIG_ENDIAN "Sets numbered parameters" + VALIDATOR cmd_validator.py <%= render "_ccsds_cmd.txt", locals: {id: 4} %> # ERB syntax: <% (1..5).each do |i| %> @@ -33,6 +37,7 @@ COMMAND <%= target_name %> SETPARAMS BIG_ENDIAN "Sets numbered parameters" POLY_WRITE_CONVERSION 0 2 COMMAND <%= target_name %> ASCIICMD BIG_ENDIAN "Enumerated ASCII command" + VALIDATOR cmd_validator.py <%= render "_ccsds_cmd.txt", locals: {id: 5} %> APPEND_PARAMETER STRING 2048 STRING "NOOP" "Enumerated string parameter" STATE "ARM LASER" "ARM LASER" HAZARDOUS "Arming the laser poses an eye safety hazard." @@ -43,16 +48,19 @@ COMMAND <%= target_name %> ASCIICMD BIG_ENDIAN "Enumerated ASCII command" RELATED_ITEM <%= target_name %> HEALTH_STATUS ASCIICMD COMMAND <%= target_name %> FLTCMD BIG_ENDIAN "Command with float parameters" + VALIDATOR cmd_validator.py <%= render "_ccsds_cmd.txt", locals: {id: 6} %> PARAMETER FLOAT32 64 32 FLOAT MIN MAX 0.0 "Float32 parameter" PARAMETER FLOAT64 96 64 FLOAT MIN MAX 0.0 "Float64 parameter" COMMAND <%= target_name %> ARYCMD BIG_ENDIAN "Command with array parameter" + VALIDATOR cmd_validator.py <%= render "_ccsds_cmd.txt", locals: {id: 7} %> ARRAY_PARAMETER ARRAY 64 32 UINT -8 "Array parameter" PARAMETER CRC -8 8 UINT MIN MAX 0 "CRC" COMMAND <%= target_name %> SLRPNLDEPLOY BIG_ENDIAN "Deploy solar array panels" + VALIDATOR cmd_validator.py <%= render "_ccsds_cmd.txt", locals: {id: 8} %> RELATED_ITEM <%= target_name %> MECH SLRPNL1 RELATED_ITEM <%= target_name %> MECH SLRPNL2 @@ -61,6 +69,7 @@ COMMAND <%= target_name %> SLRPNLDEPLOY BIG_ENDIAN "Deploy solar array panels" RELATED_ITEM <%= target_name %> MECH SLRPNL5 COMMAND <%= target_name %> SLRPNLRESET BIG_ENDIAN "Reset solar array panels" + VALIDATOR cmd_validator.py <%= render "_ccsds_cmd.txt", locals: {id: 9} %> RELATED_ITEM <%= target_name %> MECH SLRPNL1 RELATED_ITEM <%= target_name %> MECH SLRPNL2 @@ -69,17 +78,26 @@ COMMAND <%= target_name %> SLRPNLRESET BIG_ENDIAN "Reset solar array panels" RELATED_ITEM <%= target_name %> MECH SLRPNL5 COMMAND <%= target_name %> MEMLOAD BIG_ENDIAN "Load memory" + VALIDATOR cmd_validator.py DISABLE_MESSAGES # Disable messages on a command that could be sent many many times <%= render "_ccsds_cmd.txt", locals: {id: 10} %> APPEND_PARAMETER DATA 80 BLOCK "" "Block of data" RELATED_ITEM <%= target_name %> HEALTH_STATUS BLOCKTEST COMMAND <%= target_name %> QUIET BIG_ENDIAN "Enable/disable no out of limits in the demo" + VALIDATOR cmd_validator.py <%= render "_ccsds_cmd.txt", locals: {id: 11} %> APPEND_PARAMETER STATE 8 UINT 0 1 1 STATE FALSE 0 STATE TRUE 1 COMMAND <%= target_name %> TIME_OFFSET BIG_ENDIAN "Subtract the packet time by the given seconds" + VALIDATOR cmd_validator.py <%= render "_ccsds_cmd.txt", locals: {id: 12} %> APPEND_PARAMETER SECONDS 32 UINT MIN MAX 0 "Seconds to subtract from packet time" + +COMMAND <%= target_name %> HIDDEN BIG_ENDIAN "Hidden command to bump the hidden packet" + VALIDATOR cmd_validator.py + HIDDEN + <%= render "_ccsds_cmd.txt", locals: {id: 13} %> + APPEND_PARAMETER COUNT 32 UINT MIN MAX 0 "Count to set" diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/cmd_tlm/inst_tlm.txt b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/cmd_tlm/inst_tlm.txt index aa81684935..d4d15f18cd 100644 --- a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/cmd_tlm/inst_tlm.txt +++ b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/cmd_tlm/inst_tlm.txt @@ -1,5 +1,6 @@ TELEMETRY <%= target_name %> HEALTH_STATUS BIG_ENDIAN "Health and status from the <%= target_name %> target" <%= render "_ccsds_tlm.txt", locals: {apid: 1} %> + APPEND_ITEM CMD_ACPT_CNT 32 UINT "Command accept count" APPEND_ITEM COLLECTS 16 UINT "Number of collects" APPEND_ITEM TEMP1 16 UINT "Temperature #1" POLY_READ_CONVERSION -100.0 0.00305 @@ -50,7 +51,7 @@ TELEMETRY <%= target_name %> HEALTH_STATUS BIG_ENDIAN "Health and status from th STATE UNAVAILABLE 0 YELLOW APPEND_ITEM BLOCKTEST 80 BLOCK "Block data" APPEND_ITEM BRACKET[0] 8 UINT "Regular item with brackets in the name" - ITEM PACKET_TIME 0 0 DERIVED "Ruby time based on TIMESEC and TIMEUS" + ITEM PACKET_TIME 0 0 DERIVED "Python time based on TIMESEC and TIMEUS" READ_CONVERSION openc3/conversions/unix_time_conversion.py TIMESEC TIMEUS ITEM TEMP1HIGH 0 0 DERIVED "High-water mark for TEMP1" READ_CONVERSION openc3/conversions/processor_conversion.py TEMP1WATER HIGH_WATER @@ -107,7 +108,7 @@ TELEMETRY <%= target_name %> ADCS BIG_ENDIAN "Position and attitude data" FORMAT_STRING "%0.2f" ITEM ATTPROGRESS 656 32 FLOAT "Attitude file progress" FORMAT_STRING "%0.2f" - ITEM PACKET_TIME 0 0 DERIVED "Ruby time based on TIMESEC and TIMEUS" + ITEM PACKET_TIME 0 0 DERIVED "Python time based on TIMESEC and TIMEUS" READ_CONVERSION openc3/conversions/unix_time_conversion.py TIMESEC TIMEUS TELEMETRY <%= target_name %> PARAMS BIG_ENDIAN "Params set by SETPARAMS command" @@ -118,7 +119,7 @@ TELEMETRY <%= target_name %> PARAMS BIG_ENDIAN "Params set by SETPARAMS command" STATE GOOD 0 GREEN STATE BAD 1 RED <% end %> - ITEM PACKET_TIME 0 0 DERIVED "Ruby time based on TIMESEC and TIMEUS" + ITEM PACKET_TIME 0 0 DERIVED "Python time based on TIMESEC and TIMEUS" READ_CONVERSION openc3/conversions/unix_time_conversion.py TIMESEC TIMEUS TELEMETRY <%= target_name %> IMAGE BIG_ENDIAN "Packet with image data" @@ -128,7 +129,7 @@ TELEMETRY <%= target_name %> IMAGE BIG_ENDIAN "Packet with image data" ITEM BYTES 128 32 UINT "First bytes" FORMAT_STRING '0x%08x' OVERLAP # Notify OpenC3 that this is intentionally overlapping the BLOCK field - ITEM PACKET_TIME 0 0 DERIVED "Ruby time based on TIMESEC and TIMEUS" + ITEM PACKET_TIME 0 0 DERIVED "Python time based on TIMESEC and TIMEUS" READ_CONVERSION openc3/conversions/unix_time_conversion.py TIMESEC TIMEUS TELEMETRY <%= target_name %> MECH BIG_ENDIAN "Mechanism status" @@ -147,5 +148,12 @@ TELEMETRY <%= target_name %> MECH BIG_ENDIAN "Mechanism status" APPEND_ITEM CURRENT 32 FLOAT "Device current" UNITS micro-Ampères µA APPEND_ITEM STRING 0 STRING "String" - ITEM PACKET_TIME 0 0 DERIVED "Ruby time based on TIMESEC and TIMEUS" + ITEM PACKET_TIME 0 0 DERIVED "Python time based on TIMESEC and TIMEUS" + READ_CONVERSION openc3/conversions/unix_time_conversion.py TIMESEC TIMEUS + +TELEMETRY <%= target_name %> HIDDEN BIG_ENDIAN "Hidden packet" + HIDDEN + <%= render "_ccsds_tlm.txt", locals: {apid: 6} %> + APPEND_ITEM COUNT 32 UINT "Count for hidden command" + ITEM PACKET_TIME 0 0 DERIVED "Python time based on TIMESEC and TIMEUS" READ_CONVERSION openc3/conversions/unix_time_conversion.py TIMESEC TIMEUS diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/lib/cmd_validator.py b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/lib/cmd_validator.py new file mode 100644 index 0000000000..dc86ad20d2 --- /dev/null +++ b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/lib/cmd_validator.py @@ -0,0 +1,30 @@ +# Copyright 2024 OpenC3, Inc. +# All Rights Reserved. +# +# This program is free software; you can modify and/or redistribute it +# under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation; version 3 with +# attribution addendums as found in the LICENSE.txt +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# This file may also be used under the terms of a commercial license +# if purchased from OpenC3, Inc. + +from openc3.packets.command_validator import CommandValidator + + +class CmdValidator(CommandValidator): + def pre_check(self, command): + self.cmd_acpt_cnt = tlm("INST HEALTH_STATUS CMD_ACPT_CNT") + return [True, None] + + def post_check(self, command): + if command.packet_name == "CLEAR": + wait_check("INST HEALTH_STATUS CMD_ACPT_CNT == 0", 10) + else: + wait_check(f"INST HEALTH_STATUS CMD_ACPT_CNT > {self.cmd_acpt_cnt}", 10) + return [True, None] diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/lib/example_limits_response.py b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/lib/example_limits_response.py index 9decfbc35f..3395388caf 100644 --- a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/lib/example_limits_response.py +++ b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/lib/example_limits_response.py @@ -1,4 +1,4 @@ -# Copyright 2023 OpenC3, Inc. +# Copyright 2024 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -15,13 +15,16 @@ # if purchased from OpenC3, Inc. from openc3.packets.limits_response import LimitsResponse -from openc3.api.cmd_api import cmd, cmd_no_hazardous_check +from openc3.api.cmd_api import cmd class ExampleLimitsResponse(LimitsResponse): - def call(self, packet, item, old_limits_state): + def call(self, _packet, item, _old_limits_state): match item.limits.state: case "RED_HIGH": - cmd("<%= target_name %>", "COLLECT", {"TYPE": "NORMAL", "DURATION": 8}) + cmd( + "<%= target_name %> COLLECT with TYPE NORMAL, DURATION 8", + validator=False, + ) case "RED_LOW": - cmd_no_hazardous_check("<%= target_name %>", "CLEAR") + cmd("<%= target_name %> ABORT", validator=False) diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/lib/sim_inst.py b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/lib/sim_inst.py index f07b541a07..cc7de440f7 100644 --- a/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/lib/sim_inst.py +++ b/openc3-cosmos-init/plugins/packages/openc3-cosmos-demo/targets/INST2/lib/sim_inst.py @@ -1,4 +1,4 @@ -# Copyright 2023 OpenC3, Inc. +# Copyright 2024 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -165,29 +165,27 @@ def write(self, packet): match name: case "COLLECT": + hs_packet.write("cmd_acpt_cnt", hs_packet.read("cmd_acpt_cnt") + 1) hs_packet.write("collects", hs_packet.read("collects") + 1) hs_packet.write("duration", packet.read("duration")) hs_packet.write("collect_type", packet.read("type")) + case "ABORT" | "FLTCMD" | "ARYCMD": + hs_packet.write("cmd_acpt_cnt", hs_packet.read("cmd_acpt_cnt") + 1) case "CLEAR": + hs_packet.write("cmd_acpt_cnt", 0) hs_packet.write("collects", 0) - case "MEMLOAD": - hs_packet.write("blocktest", packet.read("data")) - case "QUIET": - if packet.read("state") == "TRUE": - self.quiet = True - else: - self.quiet = False - case "TIME_OFFSET": - self.time_offset = packet.read("seconds") case "SETPARAMS": + hs_packet.write("cmd_acpt_cnt", hs_packet.read("cmd_acpt_cnt") + 1) params_packet.write("value1", packet.read("value1")) params_packet.write("value2", packet.read("value2")) params_packet.write("value3", packet.read("value3")) params_packet.write("value4", packet.read("value4")) params_packet.write("value5", packet.read("value5")) case "ASCIICMD": + hs_packet.write("cmd_acpt_cnt", hs_packet.read("cmd_acpt_cnt") + 1) hs_packet.write("asciicmd", packet.read("string")) case "SLRPNLDEPLOY": + hs_packet.write("cmd_acpt_cnt", hs_packet.read("cmd_acpt_cnt") + 1) if self.solar_panel_thread and self.solar_panel_thread.is_alive(): return self.solar_panel_thread = threading.Thread( @@ -195,8 +193,24 @@ def write(self, packet): ) self.solar_panel_thread.start() case "SLRPNLRESET": + hs_packet.write("cmd_acpt_cnt", hs_packet.read("cmd_acpt_cnt") + 1) kill_thread(self, self.solar_panel_thread) self.solar_panel_positions = SimInst.SOLAR_PANEL_DFLTS[:] + case "MEMLOAD": + hs_packet.write("cmd_acpt_cnt", hs_packet.read("cmd_acpt_cnt") + 1) + hs_packet.write("blocktest", packet.read("data")) + case "QUIET": + hs_packet.write("cmd_acpt_cnt", hs_packet.read("cmd_acpt_cnt") + 1) + if packet.read("state") == "TRUE": + self.quiet = True + else: + self.quiet = False + case "TIME_OFFSET": + hs_packet.write("cmd_acpt_cnt", hs_packet.read("cmd_acpt_cnt") + 1) + self.time_offset = packet.read("seconds") + case "HIDDEN": + # Deliberately do not increment cmd_acpt_cnt + self.tlm_packets["HIDDEN"].count = packet.read("count") def solar_panel_thread_method(self): self.solar_panel_thread_cancel = False diff --git a/openc3/data/config/command_modifiers.yaml b/openc3/data/config/command_modifiers.yaml index fbb6ca7055..3fdc8f343f 100644 --- a/openc3/data/config/command_modifiers.yaml +++ b/openc3/data/config/command_modifiers.yaml @@ -248,3 +248,55 @@ VIRTUAL: summary: Marks this packet as virtual and not participating in identification description: Used for packet definitions that can be used as structures for items with a given packet. since: 5.18.0 +VALIDATOR: + summary: Defines a validator class for a command + description: Validator class is used to validate the command success or failure with both a pre_check and post_check method. + parameters: + - name: Class Filename + required: true + description: The filename which contains the Ruby or Python class. The filename must + be named after the class such that the class is a CamelCase version of the + underscored filename. For example, 'command_validator.rb' should contain + 'class CommandValidator'. + values: .* + - name: Parameter + required: false + description: Additional parameter values for the validator class which are passed + to the class constructor. + values: .* + ruby_example: | + VALIDATOR custom_validator.rb + + Defined in custom_validator.rb: + + require 'openc3/packets/command_validator' + class CustomValidator < OpenC3::CommandValidator + def pre_check(packet) + if tlm("TGT PKT ITEM") == 0 + return [false, "TGT PKT ITEM is 0"] + end + @cmd_acpt_cnt = tlm("TGT PKT CMD_ACPT_CNT") + return [true, nil] + end + def post_check(packet) + wait_check("TGT PKT CMD_ACPT_CNT > #{@cmd_acpt_cnt}", 10) + return [true, nil] + end + end + + python_example: | + VALIDATOR custom_validator.rb + + Defined in custom_validator.py: + + class CustomValidator(CommandValidator): + def pre_check(self, command): + if tlm("TGT PKT ITEM") == 0: + return [False, "TGT PKT ITEM is 0"] + self.cmd_acpt_cnt = tlm("INST HEALTH_STATUS CMD_ACPT_CNT") + return [True, None] + + def post_check(self, command): + wait_check(f"INST HEALTH_STATUS CMD_ACPT_CNT > {self.cmd_acpt_cnt}", 10) + return [True, None] + since: 5.18.0 \ No newline at end of file diff --git a/openc3/lib/openc3/api/cmd_api.rb b/openc3/lib/openc3/api/cmd_api.rb index 38abe1c547..0427b0e602 100644 --- a/openc3/lib/openc3/api/cmd_api.rb +++ b/openc3/lib/openc3/api/cmd_api.rb @@ -452,7 +452,7 @@ def _extract_target_command_parameter_names(method_name, *args) return [target_name, command_name, parameter_name] end - def _cmd_implementation(method_name, *args, range_check:, hazardous_check:, raw:, timeout: nil, log_message: nil, manual: false, + def _cmd_implementation(method_name, *args, range_check:, hazardous_check:, raw:, timeout: nil, log_message: nil, manual: false, validator: true, scope: $openc3_scope, token: $openc3_token, **kwargs) extract_string_kwargs_to_args(args, kwargs) unless [nil, true, false].include?(log_message) @@ -486,16 +486,21 @@ def _cmd_implementation(method_name, *args, range_check:, hazardous_check:, raw: cmd_params = cmd_params.transform_keys(&:upcase) user = authorize(permission: 'cmd', target_name: target_name, packet_name: cmd_name, manual: manual, scope: scope, token: token) if user.nil? - caller.each do |frame| - # Look for the following line in the stack trace which indicates custom code - # /tmp/d20240827-62-8e57pf/targets/INST/lib/example_limits_response.rb:31:in `call' - if frame.include?("/targets/#{target_name}") - user = {} - # username is the name of the custom code file - user['username'] = frame.split("/targets/")[-1].split(':')[0] - break - end - end + user = {} + user['username'] = ENV['OPENC3_MICROSERVICE_NAME'] + + # Get the caller stack trace to determine the point in the code where the command was called + # This code works but ultimately we didn't want to overload 'username' and take a performance hit + # caller.each do |frame| + # # Look for the following line in the stack trace which indicates custom code + # # /tmp/d20240827-62-8e57pf/targets/INST/lib/example_limits_response.rb:31:in `call' + # if frame.include?("/targets/#{target_name}") + # user = {} + # # username is the name of the custom code file + # user['username'] = frame.split("/targets/")[-1].split(':')[0] + # break + # end + # end end packet = TargetModel.packet(target_name, cmd_name, type: :CMD, scope: scope) if packet['disabled'] @@ -531,7 +536,8 @@ def _cmd_implementation(method_name, *args, range_check:, hazardous_check:, raw: 'hazardous_check' => hazardous_check.to_s, 'raw' => raw.to_s, 'cmd_string' => cmd_string, - 'username' => username + 'username' => username, + 'validator' => validator } CommandTopic.send_command(command, timeout: timeout, scope: scope) end diff --git a/openc3/lib/openc3/core_ext/exception.rb b/openc3/lib/openc3/core_ext/exception.rb index df2c735ee0..d44e320943 100644 --- a/openc3/lib/openc3/core_ext/exception.rb +++ b/openc3/lib/openc3/core_ext/exception.rb @@ -14,14 +14,21 @@ # GNU Affero General Public License for more details. # Modified by OpenC3, Inc. -# All changes Copyright 2022, OpenC3, Inc. +# All changes Copyright 2024, OpenC3, Inc. # All Rights Reserved # -# This file may also be used under the terms of a commercial license +# This file may also be used under the terms of a commercial license # if purchased from OpenC3, Inc. # OpenC3 specific additions to the Ruby Exception class class Exception + def filtered + backtrace = self.backtrace.select do |line| + !line.include?('lib/ruby/gems') + end + return "#{self.message}\n#{backtrace.join("\n")}" + end + # @param hide_runtime_error_class [Boolean] Whether to hide the Exception # error class if the class is RuntimeError. Other classes will continue to # be printed. diff --git a/openc3/lib/openc3/microservices/decom_microservice.rb b/openc3/lib/openc3/microservices/decom_microservice.rb index 64c00c2def..7861219374 100644 --- a/openc3/lib/openc3/microservices/decom_microservice.rb +++ b/openc3/lib/openc3/microservices/decom_microservice.rb @@ -14,7 +14,7 @@ # GNU Affero General Public License for more details. # Modified by OpenC3, Inc. -# All changes Copyright 2022, OpenC3, Inc. +# All changes Copyright 2024, OpenC3, Inc. # All Rights Reserved # # This file may also be used under the terms of a commercial license @@ -102,8 +102,17 @@ def decom_packet(_topic, msg_id, msg_hash, _redis) packet.extra = extra end packet.buffer = msg_hash["buffer"] - packet.process # Run processors - packet.check_limits(System.limits_set) # Process all the limits and call the limits_change_callback (as necessary) + # The Processor and LimitsResponse are user code points which must be rescued + # so the TelemetryDecomTopic can write the packet + begin + packet.process # Run processors + packet.check_limits(System.limits_set) # Process all the limits and call the limits_change_callback (as necessary) + rescue Exception => e + @error_count += 1 + @metric.set(name: 'decom_error_total', value: @error_count, type: 'counter') + @error = e + @logger.error e.message + end TelemetryDecomTopic.write_packet(packet, scope: @scope) diff = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start # seconds as a float @@ -149,13 +158,16 @@ def limits_change_callback(packet, item, old_limits_state, value, log_change) LimitsEventTopic.write(event, scope: @scope) if item.limits.response - begin - item.limits.response.call(packet, item, old_limits_state) - rescue Exception => e - @error = e - @logger.error "#{packet.target_name} #{packet.packet_name} #{item.name} Limits Response Exception!" - @logger.error "Called with old_state = #{old_limits_state}, new_state = #{item.limits.state}" - @logger.error e.formatted + if @thread + @thread = Thread.new do + begin + item.limits.response.call(packet, item, old_limits_state) + rescue Exception => e + @error = e + @logger.error "#{packet.target_name} #{packet.packet_name} #{item.name} Limits Response Exception!" + @logger.error "Called with old_state = #{old_limits_state}, new_state = #{item.limits.state}" + @logger.error e.filtered + end end end end diff --git a/openc3/lib/openc3/microservices/interface_microservice.rb b/openc3/lib/openc3/microservices/interface_microservice.rb index f950b921ec..10d833c409 100644 --- a/openc3/lib/openc3/microservices/interface_microservice.rb +++ b/openc3/lib/openc3/microservices/interface_microservice.rb @@ -31,6 +31,7 @@ require 'openc3/topics/command_decom_topic' require 'openc3/topics/interface_topic' require 'openc3/topics/router_topic' +require 'openc3/interfaces/interface' module OpenC3 class InterfaceCmdHandlerThread @@ -203,15 +204,44 @@ def run begin if @interface.connected? + result = true + reason = nil + if command.validator and msg_hash['validator'] + begin + result, reason = command.validator.pre_check(command) + rescue => e + result = false + reason = e.message + end + unless result + message = "pre_check returned false for #{msg_hash['cmd_string']} due to #{reason}" + raise WriteRejectError.new(message) + end + end + @count += 1 @metric.set(name: 'interface_cmd_total', value: @count, type: 'counter') if @metric - @interface.write(command) + + if command.validator and msg_hash['validator'] + begin + result, reason = command.validator.post_check(command) + rescue => e + result = false + reason = e.message + end + command.extra['post_check'] = result + command.extra['post_check_reason'] = reason if reason + end + CommandDecomTopic.write_packet(command, scope: @scope) - # Remove cmd_string from the raw packet logging - command.extra.delete('cmd_string') CommandTopic.write_packet(command, scope: @scope) InterfaceStatusModel.set(@interface.as_json(:allow_nan => true), queued: true, scope: @scope) + + unless result + message = "post_check returned false for #{msg_hash['cmd_string']} due to #{reason}" + raise WriteRejectError.new(message) + end next 'SUCCESS' else next "Interface not connected: #{@interface.name}" diff --git a/openc3/lib/openc3/packets/packet.rb b/openc3/lib/openc3/packets/packet.rb index 7df6ef01db..4748191625 100644 --- a/openc3/lib/openc3/packets/packet.rb +++ b/openc3/lib/openc3/packets/packet.rb @@ -104,6 +104,9 @@ class Packet < Structure # @return [Boolean] Whether to ignore overlapping items attr_accessor :ignore_overlap + # @return [Validator] Instance of class used to validate commands + attr_accessor :validator + # @return [Boolean] If this packet should be used for identification attr_reader :virtual @@ -148,6 +151,7 @@ def initialize(target_name = nil, packet_name = nil, default_endianness = :BIG_E @packet_time = nil @ignore_overlap = false @virtual = false + @validator = nil end # Sets the target name this packet is associated with. Unidentified packets diff --git a/openc3/lib/openc3/packets/packet_config.rb b/openc3/lib/openc3/packets/packet_config.rb index 2745bbff8e..3b5e9de8b3 100644 --- a/openc3/lib/openc3/packets/packet_config.rb +++ b/openc3/lib/openc3/packets/packet_config.rb @@ -220,7 +220,7 @@ def process_file(filename, process_target_name, language = 'ruby') 'APPEND_PARAMETER', 'APPEND_ID_ITEM', 'APPEND_ID_PARAMETER', 'APPEND_ARRAY_ITEM',\ 'APPEND_ARRAY_PARAMETER', 'ALLOW_SHORT', 'HAZARDOUS', 'PROCESSOR', 'META',\ 'DISABLE_MESSAGES', 'HIDDEN', 'DISABLED', 'VIRTUAL', 'ACCESSOR', 'TEMPLATE', 'TEMPLATE_FILE',\ - 'RESPONSE', 'ERROR_RESPONSE', 'SCREEN', 'RELATED_ITEM', 'IGNORE_OVERLAP' + 'RESPONSE', 'ERROR_RESPONSE', 'SCREEN', 'RELATED_ITEM', 'IGNORE_OVERLAP', 'VALIDATOR' raise parser.error("No current packet for #{keyword}") unless @current_packet process_current_packet(parser, keyword, params) @@ -480,22 +480,23 @@ def process_current_packet(parser, keyword, params) @current_packet.disabled = true @current_packet.virtual = true - when 'ACCESSOR' - usage = "#{keyword} " + when 'ACCESSOR', 'VALIDATOR' + usage = "#{keyword} ..." parser.verify_num_parameters(1, nil, usage) begin + keyword_equals = "#{keyword.downcase}=".to_sym if @language == 'ruby' klass = OpenC3.require_class(params[0]) if params.length > 1 - @current_packet.accessor = klass.new(@current_packet, *params[1..-1]) + @current_packet.public_send(keyword_equals, klass.new(@current_packet, *params[1..-1])) else - @current_packet.accessor = klass.new(@current_packet) + @current_packet.public_send(keyword_equals, klass.new(@current_packet)) end else if params.length > 1 - @current_packet.accessor = PythonProxy.new('Accessor', params[0], @current_packet, *params[1..-1]) + @current_packet.public_send(keyword_equals, PythonProxy.new(keyword.capitalize, params[0], @current_packet, *params[1..-1])) else - @current_packet.accessor = PythonProxy.new('Accessor', params[0], @current_packet) + @current_packet.public_send(keyword_equals, PythonProxy.new(keyword.capitalize, params[0], @current_packet)) end end rescue Exception => e diff --git a/openc3/lib/openc3/script/api_shared.rb b/openc3/lib/openc3/script/api_shared.rb index e782720200..a5d00969f5 100644 --- a/openc3/lib/openc3/script/api_shared.rb +++ b/openc3/lib/openc3/script/api_shared.rb @@ -14,13 +14,14 @@ # GNU Affero General Public License for more details. # Modified by OpenC3, Inc. -# All changes Copyright 2023, OpenC3, Inc. +# All changes Copyright 2024, OpenC3, Inc. # All Rights Reserved # # This file may also be used under the terms of a commercial license # if purchased from OpenC3, Inc. require 'openc3/script/extract' +require 'openc3/script/exceptions' module OpenC3 module ApiShared diff --git a/openc3/lib/openc3/topics/command_topic.rb b/openc3/lib/openc3/topics/command_topic.rb index 1f4c5fa1ef..53556a2934 100644 --- a/openc3/lib/openc3/topics/command_topic.rb +++ b/openc3/lib/openc3/topics/command_topic.rb @@ -14,7 +14,7 @@ # GNU Affero General Public License for more details. # Modified by OpenC3, Inc. -# All changes Copyright 2022, OpenC3, Inc. +# All changes Copyright 2024, OpenC3, Inc. # All Rights Reserved # # This file may also be used under the terms of a commercial license @@ -26,7 +26,7 @@ module OpenC3 class CommandTopic < Topic - COMMAND_ACK_TIMEOUT_S = 5 + COMMAND_ACK_TIMEOUT_S = 30 def self.write_packet(packet, scope:) topic = "#{scope}__COMMAND__{#{packet.target_name}}__#{packet.packet_name}" @@ -52,7 +52,7 @@ def self.send_command(command, timeout: COMMAND_ACK_TIMEOUT_S, scope:) cmd_id = Topic.write_topic("{#{scope}__CMD}TARGET__#{command['target_name']}", command, '*', 100) time = Time.now while (Time.now - time) < timeout - Topic.read_topics([ack_topic]) do |topic, msg_id, msg_hash, redis| + Topic.read_topics([ack_topic]) do |_topic, _msg_id, msg_hash, _redis| if msg_hash["id"] == cmd_id if msg_hash["result"] == "SUCCESS" return [command['target_name'], command['cmd_name'], cmd_params] diff --git a/openc3/lib/openc3/topics/decom_interface_topic.rb b/openc3/lib/openc3/topics/decom_interface_topic.rb index 746d397a8f..d0e805bd8c 100644 --- a/openc3/lib/openc3/topics/decom_interface_topic.rb +++ b/openc3/lib/openc3/topics/decom_interface_topic.rb @@ -37,7 +37,7 @@ def self.build_cmd(target_name, cmd_name, cmd_params, range_check, raw, scope:) timeout = 5 # Arbitrary 5s timeout time = Time.now while (Time.now - time) < timeout - Topic.read_topics([ack_topic]) do |topic, msg_id, msg_hash, redis| + Topic.read_topics([ack_topic]) do |_topic, _msg_id, msg_hash, _redis| if msg_hash["id"] == decom_id if msg_hash["result"] == "SUCCESS" return msg_hash diff --git a/openc3/python/openc3/api/cmd_api.py b/openc3/python/openc3/api/cmd_api.py index 1c8b44393e..3b9148f3c1 100644 --- a/openc3/python/openc3/api/cmd_api.py +++ b/openc3/python/openc3/api/cmd_api.py @@ -14,7 +14,7 @@ # This file may also be used under the terms of a commercial license # if purchased from OpenC3, Inc. -import traceback +import os from contextlib import contextmanager from openc3.api import WHITELIST from openc3.api.interface_api import get_interface @@ -584,15 +584,20 @@ def _cmd_implementation( cmd_params = {k.upper(): v for k, v in cmd_params.items()} user = authorize(permission="cmd", target_name=target_name, packet_name=cmd_name, scope=scope) if not user: - stack_trace = traceback.extract_stack() - for frame in stack_trace: - # Look for the following line in the stack trace which indicates custom code - # File "/tmp/tmpp96e6j83/targets/INST2/lib/example_limits_response.py", line 25, in call" - if f"/targets/{target_name}" in frame.filename: - user = {} - # username is the name of the custom code file - user["username"] = frame.filename.split("/targets/")[-1].split('"')[0] - break + user = {} + user["username"] = os.environ.get("OPENC3_MICROSERVICE_NAME") + + # Get the caller stack trace to determine the point in the code where the command was called + # This code works but ultimately we didn't want to overload 'username' and take a performance hit + # stack_trace = traceback.extract_stack() + # for frame in stack_trace: + # # Look for the following line in the stack trace which indicates custom code + # # File "/tmp/tmpp96e6j83/targets/INST2/lib/example_limits_response.py", line 25, in call" + # if f"/targets/{target_name}" in frame.filename: + # user = {} + # # username is the name of the custom code file + # user["username"] = frame.filename.split("/targets/")[-1].split('"')[0] + # break packet = TargetModel.packet(target_name, cmd_name, type="CMD", scope=scope) if packet.get("disabled", False): @@ -645,7 +650,7 @@ def _cmd_implementation( "range_check": str(range_check), "hazardous_check": str(hazardous_check), "raw": str(raw), - "cmd_stirng": cmd_string, + "cmd_string": cmd_string, "username": username, } return CommandTopic.send_command(command, timeout, scope) @@ -662,30 +667,29 @@ def _cmd_log_string(method_name, target_name, cmd_name, cmd_params, packet): if key in Packet.RESERVED_ITEM_NAMES: continue - found = False - for item in packet["items"]: - if item["name"] == key: - found = item - break - if found and "data_type" in found: - item_type = found["data_type"] - else: - item_type = None - - if isinstance(value, str): - if item_type == "BLOCK" or item_type == "STRING": - if not value.isascii(): - value = "0x" + simple_formatted(value) - else: - value = f"'{str(value)}'" + found = False + for item in packet["items"]: + if item["name"] == key: + found = item + break + if found and "data_type" in found: + item_type = found["data_type"] else: - value = convert_to_value(value) - if len(value) > 256: - value = value[:256] + "...'" - value = value.replace('"', "'") - elif isinstance(value, list): - value = f"[{', '.join(str(i) for i in value)}]" - params.append(f"{key} {value}") - params = ", ".join(params) - output_string += " with " + params + '")' + item_type = None + + if isinstance(value, str): + if item_type == "BLOCK" or item_type == "STRING": + if not value.isascii(): + value = "0x" + simple_formatted(value) + else: + value = f"'{str(value)}'" + else: + value = convert_to_value(value) + if len(value) > 256: + value = value[:256] + "...'" + value = value.replace('"', "'") + elif isinstance(value, list): + value = f"[{', '.join(str(i) for i in value)}]" + params.append(f"{key} {value}") + output_string += " with " + ", ".join(params) + '")' return output_string diff --git a/openc3/python/openc3/interfaces/interface.py b/openc3/python/openc3/interfaces/interface.py index 1911c5416b..06a13d260b 100644 --- a/openc3/python/openc3/interfaces/interface.py +++ b/openc3/python/openc3/interfaces/interface.py @@ -26,9 +26,6 @@ from openc3.utilities.secrets import Secrets from openc3.logs.stream_log_pair import StreamLogPair -# TODO: -# require 'openc3/api/api' - class WriteRejectError(RuntimeError): """Define a class to allow interfaces and protocols to reject commands without disconnecting the interface""" @@ -275,7 +272,7 @@ def _write(self): try: yield except WriteRejectError as error: - Logger.error(f"{self.name}: Write rejected by interface {error.message}") + Logger.error(f"{self.name}: Write rejected by interface {repr(error)}") raise error except RuntimeError as error: Logger.error(f"{self.name}: Error writing to interface") @@ -409,6 +406,7 @@ def write_interface_base(self, data, extra=None): self.written_raw_data_time = datetime.now(timezone.utc) self.written_raw_data = data self.bytes_written += len(data) + print(f"bytes_written: {self.bytes_written}") if self.stream_log_pair: self.stream_log_pair.write_log.write(data) diff --git a/openc3/python/openc3/microservices/decom_microservice.py b/openc3/python/openc3/microservices/decom_microservice.py index c14cb3432d..e3e4ae6254 100644 --- a/openc3/python/openc3/microservices/decom_microservice.py +++ b/openc3/python/openc3/microservices/decom_microservice.py @@ -1,4 +1,4 @@ -# Copyright 2023 OpenC3, Inc. +# Copyright 2024 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -71,7 +71,7 @@ def run(self): self.metric.set(name="decom_total", value=self.count, type="counter") self.count += 1 LimitsEventTopic.sync_system_thread_body(scope=self.scope) - except RuntimeError as error: + except Exception as error: self.error_count += 1 self.metric.set(name="decom_error_total", value=self.error_count, type="counter") self.error = error @@ -102,10 +102,18 @@ def decom_packet(self, topic, msg_id, msg_hash, _redis): if extra is not None: packet.extra = json.loads(extra) packet.buffer = msg_hash[b"buffer"] - packet.process() # Run processors - packet.check_limits( - System.limits_set() - ) # Process all the limits and call the limits_change_callback (as necessary) + # The Processor and LimitsResponse are user code points which must be rescued + # so the TelemetryDecomTopic can write the packet + try: + packet.process() # Run processors + packet.check_limits( + System.limits_set() + ) # Process all the limits and call the limits_change_callback (as necessary) + except Exception as error: + self.error_count += 1 + self.metric.set(name="decom_error_total", value=self.error_count, type="counter") + self.error = error + self.logger.error(repr(error)) TelemetryDecomTopic.write_packet(packet, scope=self.scope) diff = time.time() - start # seconds as a float @@ -161,7 +169,7 @@ def limits_change_callback(self, packet, item, old_limits_state, value, log_chan if item.limits.response is not None: try: item.limits.response.call(packet, item, old_limits_state) - except RuntimeError as error: + except Exception as error: self.error = error self.logger.error(f"{packet.target_name} {packet.packet_name} {item.name} Limits Response Exception!") self.logger.error(f"Called with old_state = {old_limits_state}, new_state = {item.limits.state}") diff --git a/openc3/python/openc3/microservices/interface_microservice.py b/openc3/python/openc3/microservices/interface_microservice.py index e0cb3567a5..9aa3c950c1 100644 --- a/openc3/python/openc3/microservices/interface_microservice.py +++ b/openc3/python/openc3/microservices/interface_microservice.py @@ -149,7 +149,7 @@ def process_cmd(self, topic, msg_id, msg_hash, redis): InterfaceStatusModel.set(self.interface.as_json(), queued=True, scope=self.scope) except RuntimeError as error: self.logger.error(f"{self.interface.name}: interface_cmd: {repr(error)}") - return error.message + return error.str() return "SUCCESS" if msg_hash.get(b"protocol_cmd"): params = json.loads(msg_hash[b"protocol_cmd"]) @@ -167,7 +167,7 @@ def process_cmd(self, topic, msg_id, msg_hash, redis): InterfaceStatusModel.set(self.interface.as_json(), queued=True, scope=self.scope) except RuntimeError as error: self.logger.error(f"{self.interface.name}: protocol_cmd:{repr(error)}") - return error.message + return error.str() return "SUCCESS" if msg_hash.get(b"inject_tlm"): handle_inject_tlm(msg_hash[b"inject_tlm"], self.scope) @@ -208,33 +208,61 @@ def process_cmd(self, topic, msg_id, msg_hash, redis): self.logger.error(f"{self.interface.name}: {repr(error)}") return repr(error) - command.extra = {} or command.extra - command.extra["cmd_string"] = msg_hash["cmd_string"] - command.extra["username"] = msg_hash["username"] + command.extra = command.extra or {} + print(f"msg_hash:{msg_hash}") + command.extra["cmd_string"] = msg_hash[b"cmd_string"] + command.extra["username"] = msg_hash[b"username"] + print(f"extra:{command.extra}") if hazardous_check: hazardous, hazardous_description = System.commands.cmd_pkt_hazardous(command) # Return back the error, description, and the formatted command # This allows the error handler to simply re-send the command if hazardous: - return f"HazardousError\n{hazardous_description}\n{msg_hash["cmd_string"]}" + return f"HazardousError\n{hazardous_description}\n{msg_hash[b'cmd_string']}" try: if self.interface.connected(): + result = True + reason = None + if command.validator: + try: + result, reason = command.validator.pre_check(command) + except Exception as error: + result = False + reason = repr(error) + if not result: + message = f"pre_check returned false for {command.extra['cmd_string']} due to {reason}" + raise WriteRejectError(message) + self.count += 1 if self.metric is not None: self.metric.set(name="interface_cmd_total", value=self.count, type="counter") - self.interface.write(command) + + if command.validator: + try: + result, reason = command.validator.post_check(command) + except Exception as error: + result = False + reason = repr(error) + command.extra["post_check"] = result + if reason: + command.extra["post_check_reason"] = reason + CommandDecomTopic.write_packet(command, scope=self.scope) - # Remove cmd_string from the raw packet logging - command.extra.pop("cmd_string") CommandTopic.write_packet(command, scope=self.scope) + print(f"interface json:{self.interface.as_json()}") InterfaceStatusModel.set(self.interface.as_json(), queued=True, scope=self.scope) + + if not result: + message = f"post_check returned false for {command.extra['cmd_string']} due to {reason}" + raise WriteRejectError(message) + return "SUCCESS" else: return f"Interface not connected: {self.interface.name}" except WriteRejectError as error: - return error.message + return error.str() except RuntimeError as error: self.logger.error(f"{self.interface.name}: {repr(error)}") return repr(error) @@ -323,7 +351,7 @@ def run(self): self.router.interface_cmd(params["cmd_name"], *params["cmd_params"]) except RuntimeError as error: self.logger.error(f"{self.router.name}: router_cmd: {repr(error)}") - return error.message + return error.str() return "SUCCESS" if msg_hash.get(b"protocol_cmd"): params = json.loads(msg_hash[b"protocol_cmd"]) @@ -339,7 +367,7 @@ def run(self): ) except RuntimeError as error: self.logger.error(f"{self.router.name}: protoco_cmd: {repr(error)}") - return error.message + return error.str() return "SUCCESS" return "SUCCESS" @@ -363,7 +391,7 @@ def run(self): return "SUCCESS" except RuntimeError as error: self.logger.error(f"{self.router.name}: {repr(error)}") - return error.message + return error.str() class InterfaceMicroservice(Microservice): @@ -470,9 +498,7 @@ def attempting(self, *params): return self.interface # Return the interface/router since we may have recreated it # Need to rescue Exception so we cover LoadError except RuntimeError as error: - self.logger.error( - f"Attempting connection #{self.interface.connection_string} failed due to {error.message}" - ) + self.logger.error(f"Attempting connection #{self.interface.connection_string} failed due to {repr(error)}") # if SignalException === error: # self.logger.info(f"{self.interface.name}: Closing from signal") # self.cancel_thread = True @@ -619,9 +645,7 @@ def handle_connection_failed(self, connection, connect_error): # case Errno='ECONNREFUSED', Errno='ECONNRESET', Errno='ETIMEDOUT', Errno='ENOTSOCK', Errno='EHOSTUNREACH', IOError: # # Do not write an exception file for these extremely common cases # else _: - if connect_error is RuntimeError and ( - "canceled" in connect_error.message or "timeout" in connect_error.message - ): + if connect_error is RuntimeError and ("canceled" in connect_error.message or "timeout" in connect_error.str()): pass # Do not write an exception file for these extremely common cases else: self.logger.error(f"{self.interface.name}: {str(connect_error)}") diff --git a/openc3/python/openc3/packets/packet.py b/openc3/python/openc3/packets/packet.py index a11bb9c1cb..7ca1110e40 100644 --- a/openc3/python/openc3/packets/packet.py +++ b/openc3/python/openc3/packets/packet.py @@ -97,6 +97,7 @@ def __init__( self.related_items = None self.ignore_overlap = False self.virtual = False + self.validator = None @property def target_name(self): diff --git a/openc3/python/openc3/packets/packet_config.py b/openc3/python/openc3/packets/packet_config.py index ad22027146..8d7f612d90 100644 --- a/openc3/python/openc3/packets/packet_config.py +++ b/openc3/python/openc3/packets/packet_config.py @@ -209,6 +209,7 @@ def process_file(self, filename, process_target_name): | "DISABLED" | "VIRTUAL" | "ACCESSOR" + | "VALIDATOR" | "TEMPLATE" | "TEMPLATE_FILE" | "RESPONSE" @@ -455,7 +456,7 @@ def process_current_packet(self, parser, keyword, params): self.current_packet.virtual = True case "ACCESSOR": - usage = f"{keyword} " + usage = f"{keyword} ..." parser.verify_num_parameters(1, None, usage) try: filename = class_name_to_filename(params[0]) @@ -467,6 +468,21 @@ def process_current_packet(self, parser, keyword, params): except ModuleNotFoundError as error: raise parser.error(error) + case "VALIDATOR": + usage = f"{keyword} ..." + parser.verify_num_parameters(1, None, usage) + try: + klass = get_class_from_module( + filename_to_module(params[0]), + filename_to_class_name(params[0]), + ) + if len(params) > 1: + self.current_packet.validator = klass(self.current_packet, *params[1:]) + else: + self.current_packet.validator = klass(self.current_packet) + except ModuleNotFoundError as error: + raise parser.error(error) + case "TEMPLATE": usage = f"{keyword}