openc3 6.6.0 → 6.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +77 -16
  3. data/data/config/command_modifiers.yaml +3 -3
  4. data/data/config/interface_modifiers.yaml +1 -1
  5. data/data/config/table_manager.yaml +1 -1
  6. data/data/config/telemetry_modifiers.yaml +3 -3
  7. data/data/config/widgets.yaml +2 -2
  8. data/lib/openc3/accessors.rb +1 -1
  9. data/lib/openc3/api/cmd_api.rb +15 -4
  10. data/lib/openc3/api/settings_api.rb +8 -0
  11. data/lib/openc3/api/stash_api.rb +1 -1
  12. data/lib/openc3/api/tlm_api.rb +96 -14
  13. data/lib/openc3/core_ext/kernel.rb +3 -3
  14. data/lib/openc3/logs/log_writer.rb +16 -12
  15. data/lib/openc3/microservices/interface_microservice.rb +14 -1
  16. data/lib/openc3/microservices/plugin_microservice.rb +2 -2
  17. data/lib/openc3/microservices/queue_microservice.rb +166 -0
  18. data/lib/openc3/models/cvt_model.rb +140 -3
  19. data/lib/openc3/models/plugin_model.rb +7 -2
  20. data/lib/openc3/models/plugin_store_model.rb +70 -0
  21. data/lib/openc3/models/queue_model.rb +232 -0
  22. data/lib/openc3/models/target_model.rb +26 -0
  23. data/lib/openc3/models/tool_model.rb +1 -1
  24. data/lib/openc3/packets/packet.rb +3 -3
  25. data/lib/openc3/packets/parsers/state_parser.rb +7 -1
  26. data/lib/openc3/packets/structure.rb +9 -2
  27. data/lib/openc3/script/calendar.rb +10 -10
  28. data/lib/openc3/script/commands.rb +4 -4
  29. data/lib/openc3/script/queue.rb +80 -0
  30. data/lib/openc3/script/script.rb +1 -0
  31. data/lib/openc3/script/script_runner.rb +7 -2
  32. data/lib/openc3/script/tables.rb +3 -3
  33. data/lib/openc3/script/web_socket_api.rb +11 -0
  34. data/lib/openc3/topics/queue_topic.rb +29 -0
  35. data/lib/openc3/utilities/authorization.rb +1 -1
  36. data/lib/openc3/utilities/cosmos_rails_formatter.rb +1 -1
  37. data/lib/openc3/utilities/local_mode.rb +2 -0
  38. data/lib/openc3/utilities/logger.rb +1 -1
  39. data/lib/openc3/utilities/running_script.rb +5 -1
  40. data/lib/openc3/version.rb +5 -5
  41. data/templates/tool_angular/package.json +2 -2
  42. data/templates/tool_react/package.json +1 -1
  43. data/templates/tool_svelte/package.json +1 -1
  44. data/templates/tool_vue/package.json +3 -3
  45. data/templates/widget/package.json +2 -2
  46. metadata +83 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d08bcc3b92811101e2096aa443b5459835d0396aba80b1067c8147388ec050e6
4
- data.tar.gz: 2820795b23e3da0530d0306ee2665d69bb5ad86f494609ced199f985929c56f2
3
+ metadata.gz: 0b3a2017659bdea9209911793ce6d8f53c28320df77e9c938e1a3ec6d0e88527
4
+ data.tar.gz: 950e4c86f740f303186262aa4c808f063219b7097e4b1a0c343aa83da9a71b48
5
5
  SHA512:
6
- metadata.gz: 181ce42f3c55f78ee8e6adf3cc7145326974c8239a82478b501def91314169cacb62a9e53c4d0d760e0b0125a1b77e437127f7992d7e4fe42838035640b87dbe
7
- data.tar.gz: 957334977c15e7d8e3fe49d14bec6c2e11c7cba09bbaea70cefe3f85a408255f1e7f2620b642c6f7a137d6ff9bdc1b4b49007fca58a9c6c5f9562b81daa0263e
6
+ metadata.gz: a6bcbc2924b4b7b0e7d961a0892f5e74fbb527998527f3c1ae1d7ba4c3213cbda5088864383c25bbebdeca01b09571efb596248d892fb513dc30181bbf376b1c
7
+ data.tar.gz: 0f42338482eb4069675bee41a29b0396ced047520a7300f7a627dcca2ff0fc483536448860f82e6e99a44a42b09ce64b261604fc35dd586b53ac764352ac9118
data/bin/openc3cli CHANGED
@@ -24,17 +24,18 @@
24
24
  # This file will handle OpenC3 tasks such as instantiating a new project
25
25
 
26
26
  require 'openc3'
27
- require 'openc3/utilities/local_mode'
28
- require 'openc3/utilities/bucket'
29
- require 'openc3/utilities/cli_generator'
30
- require 'openc3/models/scope_model'
31
- require 'openc3/models/plugin_model'
27
+ require 'openc3/bridge/bridge'
32
28
  require 'openc3/models/gem_model'
33
29
  require 'openc3/models/migration_model'
30
+ require 'openc3/models/plugin_model'
34
31
  require 'openc3/models/python_package_model'
32
+ require 'openc3/models/queue_model'
33
+ require 'openc3/models/scope_model'
35
34
  require 'openc3/models/tool_model'
36
35
  require 'openc3/packets/packet_config'
37
- require 'openc3/bridge/bridge'
36
+ require 'openc3/utilities/bucket'
37
+ require 'openc3/utilities/cli_generator'
38
+ require 'openc3/utilities/local_mode'
38
39
  require 'ostruct'
39
40
  require 'optparse'
40
41
  require 'openc3/utilities/zip'
@@ -63,8 +64,9 @@ def print_usage
63
64
  puts " cli rake # Runs rake in the local directory"
64
65
  puts " cli irb # Runs irb in the local directory"
65
66
  puts " cli script # Interact with scripts. Run with --help for more info."
66
- puts " cli validate /PATH/FILENAME.gem SCOPE variables.txt # Validate a COSMOS plugin gem file"
67
- puts " cli load /PATH/FILENAME.gem SCOPE variables.txt # Loads a COSMOS plugin gem file"
67
+ puts " cli validate /PATH/FILENAME.gem SCOPE variables.json # Validate a COSMOS plugin gem file"
68
+ puts " cli load /PATH/FILENAME.gem SCOPE plugin_hash.json # Loads a COSMOS plugin gem file"
69
+ puts " OPTIONS: --variables lets you pass a path to a JSON file containing your plugin's variables"
68
70
  puts " cli list <SCOPE> # Lists installed plugins, SCOPE is DEFAULT if not given"
69
71
  puts " cli generate TYPE OPTIONS # Generate various COSMOS entities"
70
72
  puts " OPTIONS: --ruby or --python is required to specify the language in the generated code unless OPENC3_LANGUAGE is set"
@@ -333,7 +335,7 @@ end
333
335
  # Pass true as the last argument to force install even if a plugin with
334
336
  # the same version number exists
335
337
  #
336
- def load_plugin(plugin_file_path, scope:, plugin_hash_file: nil, force: false)
338
+ def load_plugin(plugin_file_path, scope:, plugin_hash_file: nil, force: false, variables_file: nil)
337
339
  scope ||= 'DEFAULT'
338
340
  check_environment()
339
341
  if $openc3_in_cluster
@@ -352,9 +354,10 @@ def load_plugin(plugin_file_path, scope:, plugin_hash_file: nil, force: false)
352
354
  end
353
355
 
354
356
  begin
357
+ existing_variables = JSON.parse(File.read(variables_file)) if variables_file
355
358
  if plugin_hash_file
356
359
  # Admin Create / Edit / or Upgrade Plugin
357
- OpenC3::PluginModel.install_phase1(plugin_file_path, scope: scope)
360
+ OpenC3::PluginModel.install_phase1(plugin_file_path, existing_variables: existing_variables, scope: scope)
358
361
  plugin_hash = JSON.parse(File.read(plugin_hash_file), :allow_nan => true, :create_additions => true)
359
362
  else
360
363
  # Init or Command Line openc3cli load with no plugin_hash_file
@@ -378,7 +381,7 @@ def load_plugin(plugin_file_path, scope:, plugin_hash_file: nil, force: false)
378
381
  end
379
382
  return if found
380
383
 
381
- plugin_hash = OpenC3::PluginModel.install_phase1(plugin_file_path, scope: scope)
384
+ plugin_hash = OpenC3::PluginModel.install_phase1(plugin_file_path, existing_variables: existing_variables, scope: scope)
382
385
  end
383
386
 
384
387
  # Determine if plugin named in plugin_hash exists
@@ -640,12 +643,39 @@ def cli_script_list(args, options)
640
643
  return 0
641
644
  end
642
645
 
646
+ def parse_suite_runner_options(options)
647
+ suite_runner = nil
648
+ # suite must be given to enable Suite Runner execution
649
+ if options[:suite]
650
+ suite_runner = {}
651
+ suite_runner['suite'] = options[:suite]
652
+ if options[:group]
653
+ suite_runner['group'] = options[:group]
654
+ # script requires group to be set
655
+ if options[:script]
656
+ suite_runner['script'] = options[:script]
657
+ end
658
+ end
659
+ if options[:method]
660
+ suite_runner['method'] = options[:method]
661
+ else
662
+ suite_runner['method'] = 'start'
663
+ end
664
+ if options[:options]
665
+ suite_runner['options'] = options[:options].split(',')
666
+ else
667
+ suite_runner['options'] = ["continueAfterError"]
668
+ end
669
+ end
670
+ return suite_runner
671
+ end
672
+
643
673
  def cli_script_run(args, options)
644
674
  environment = get_env_from_args(args)
645
-
675
+ suite_runner = parse_suite_runner_options(options)
646
676
  ret_code = ERROR_CODE
647
677
  require 'openc3/script'
648
- if (id = script_run(args[1], disconnect: options[:disconnect], environment: environment, scope: options[:scope]))
678
+ if (id = script_run(args[1], disconnect: options[:disconnect], environment: environment, suite_runner: suite_runner, scope: options[:scope]))
649
679
  puts id
650
680
  $script_interrupt_text = " Script #{args[1]} still running remotely.\n" # for Ctrl-C
651
681
  if (options[:wait] < 1) then
@@ -673,10 +703,10 @@ end
673
703
 
674
704
  def cli_script_spawn(args, options)
675
705
  environment = get_env_from_args(args)
676
-
706
+ suite_runner = parse_suite_runner_options(options)
677
707
  ret_code = ERROR_CODE
678
708
  require 'openc3/script'
679
- if (id = script_run(args[1], disconnect: options[:disconnect], environment: environment, scope: options[:scope]))
709
+ if (id = script_run(args[1], disconnect: options[:disconnect], environment: environment, suite_runner: suite_runner, scope: options[:scope]))
680
710
  puts id
681
711
  ret_code = 0
682
712
  end
@@ -769,6 +799,22 @@ def cli_script(args=[])
769
799
  opts.on("--scope SCOPE", "Run with specified scope (default = DEFAULT)") do |arg|
770
800
  options[:scope] = arg
771
801
  end
802
+ opts.on("--suite SUITE", "Run with specified suite") do |arg|
803
+ options[:suite] = arg
804
+ end
805
+ opts.on("--group GROUP", "Run with specified group") do |arg|
806
+ options[:group] = arg
807
+ end
808
+ opts.on("--script SCRIPT", "Run with specified script") do |arg|
809
+ options[:script] = arg
810
+ end
811
+ opts.on("--method start", "Method must be start, setup, or teardown. Default is start.") do |arg|
812
+ options[:method] = arg
813
+ end
814
+ # TODO 7.0: Should these all be snake case?
815
+ opts.on("--options options", "Options is a comma separated list consisting of continueAfterError,pauseOnError,abortAfterError,manual,loop,breakLoopOnError. Default is continueAfterError.") do |arg|
816
+ options[:options] = arg
817
+ end
772
818
  opts.on("-d", "--disconnect", "Run a script in disconnect mode (default = false)") do |arg|
773
819
  options[:disconnect] = arg
774
820
  end
@@ -848,7 +894,18 @@ if not ARGV[0].nil? # argument(s) given
848
894
  when 'load'
849
895
  # force is a boolean so if they pass 'force' it is true
850
896
  # See plugins_controller.rb install for usage
851
- load_plugin(ARGV[1], scope: ARGV[2], plugin_hash_file: ARGV[3], force: ARGV[4] == 'force')
897
+ variables_option = ARGV.find_index('--variables')
898
+ if variables_option.nil?
899
+ scope = ARGV[2]
900
+ plugin_hash_file = ARGV[3]
901
+ force = ARGV[4] == 'force'
902
+ else
903
+ scope = ARGV[2] unless variables_option <= 2
904
+ plugin_hash_file = ARGV[3] unless variables_option <= 3
905
+ force = ARGV[4] == 'force' unless variables_option <= 4
906
+ variables_file = ARGV[variables_option + 1]
907
+ end
908
+ load_plugin(ARGV[1], scope: scope, plugin_hash_file: plugin_hash_file, force: force, variables_file: variables_file)
852
909
 
853
910
  when 'list'
854
911
  list_plugins(scope: ARGV[1])
@@ -959,6 +1016,10 @@ if not ARGV[0].nil? # argument(s) given
959
1016
  end
960
1017
  end
961
1018
 
1019
+ when 'createqueue'
1020
+ queue = OpenC3::QueueModel.new(name: ARGV[1], state: 'RELEASE', scope: ARGV[2])
1021
+ queue.create
1022
+
962
1023
  when 'destroyscope'
963
1024
  scope = OpenC3::ScopeModel.get_model(name: ARGV[1])
964
1025
  scope.destroy
@@ -11,7 +11,7 @@ PARAMETER:
11
11
  - name: Bit Offset
12
12
  required: true
13
13
  description: Bit offset into the command packet of the Most Significant Bit of this parameter.
14
- May be negative to indicate on offset from the end of the packet.
14
+ May be negative to indicate an offset from the end of the packet.
15
15
  Always use a bit offset of 0 for derived parameters.
16
16
  values: '[-]?\d+'
17
17
  <%= MetaConfigParser.load('_params.yaml').to_meta_config_yaml(4) %>
@@ -50,7 +50,7 @@ ID_PARAMETER:
50
50
  - name: Bit Offset
51
51
  required: true
52
52
  description: Bit offset into the command packet of the Most Significant Bit of this parameter.
53
- May be negative to indicate on offset from the end of the packet.
53
+ May be negative to indicate an offset from the end of the packet.
54
54
  values: '[-]?\d+'
55
55
  <%= MetaConfigParser.load('_id_params.yaml').to_meta_config_yaml(4) %>
56
56
  example: ID_PARAMETER OPCODE 32 32 UINT 2 2 2 "Opcode identifier"
@@ -80,7 +80,7 @@ ARRAY_PARAMETER:
80
80
  - name: Bit Offset
81
81
  required: true
82
82
  description: Bit offset into the command packet of the Most Significant Bit of this parameter.
83
- May be negative to indicate on offset from the end of the packet.
83
+ May be negative to indicate an offset from the end of the packet.
84
84
  Always use a bit offset of 0 for derived parameters.
85
85
  values: '[-]?\d+'
86
86
  <%= MetaConfigParser.load('_array_params.yaml').to_meta_config_yaml(4) %>
@@ -124,7 +124,7 @@ PROTOCOL:
124
124
  python_example: |
125
125
  INTERFACE DATA_INT openc3/interfaces/tcpip_client_interface.py host.docker.internal 8080 8081 10.0 nil BURST
126
126
  MAP_TARGET DATA
127
- PROTOCOL READ IgnorePacketProtocol INST IMAGE # Drop all INST IMAGE packets
127
+ PROTOCOL READ openc3/interfaces/protocols/ignore_packet_protocol.py INST IMAGE # Drop all INST IMAGE packets
128
128
  OPTION:
129
129
  summary: Set a parameter on an interface
130
130
  description:
@@ -22,7 +22,7 @@ TABLE:
22
22
  - name: Bit Offset
23
23
  required: true
24
24
  description: Bit offset into the table of the Most Significant Bit of this parameter.
25
- May be negative to indicate on offset from the end of the table.
25
+ May be negative to indicate an offset from the end of the table.
26
26
  Always use a bit offset of 0 for derived parameters.
27
27
  values: '[-]?\d+'
28
28
  <%= MetaConfigParser.load('_params.yaml').to_meta_config_yaml(8) %>
@@ -14,7 +14,7 @@ ITEM:
14
14
  - name: Bit Offset
15
15
  required: true
16
16
  description: Bit offset into the telemetry packet of the Most Significant Bit of this item.
17
- May be negative to indicate on offset from the end of the packet.
17
+ May be negative to indicate an offset from the end of the packet.
18
18
  Always use a bit offset of 0 for derived item.
19
19
  values: '[-]?\d+'
20
20
  <%= MetaConfigParser.load('_items.yaml').to_meta_config_yaml(4) %>
@@ -44,7 +44,7 @@ ID_ITEM:
44
44
  - name: Bit Offset
45
45
  required: true
46
46
  description: Bit offset into the telemetry packet of the Most Significant Bit of this item.
47
- May be negative to indicate on offset from the end of the packet.
47
+ May be negative to indicate an offset from the end of the packet.
48
48
  values: '[-]?\d+'
49
49
  <%= MetaConfigParser.load('_id_items.yaml').to_meta_config_yaml(4) %>
50
50
  APPEND_ID_ITEM:
@@ -71,7 +71,7 @@ ARRAY_ITEM:
71
71
  - name: Bit Offset
72
72
  required: true
73
73
  description: Bit offset into the telemetry packet of the Most Significant Bit of this item.
74
- May be negative to indicate on offset from the end of the packet.
74
+ May be negative to indicate an offset from the end of the packet.
75
75
  Always use a bit offset of 0 for derived item.
76
76
  values: '[-]?\d+'
77
77
  <%= MetaConfigParser.load('_array_params.yaml').to_meta_config_yaml(4) %>
@@ -746,11 +746,11 @@ Telemetry Widgets:
746
746
  values: <%= %w(RAW CONVERTED FORMATTED WITH_UNITS) %>
747
747
  - name: Width
748
748
  required: false
749
- description: Width of the LED circle (default = 15)
749
+ description: Width of the LED circle (default = 20)
750
750
  values: .*
751
751
  - name: Height
752
752
  required: false
753
- description: Height of the LED circle (default = 15)
753
+ description: Height of the LED circle (default = 20)
754
754
  values: .*
755
755
  example: |
756
756
  LED INST PARAMS VALUE5 RAW 25 20 # Ellipse
@@ -1,6 +1,6 @@
1
1
  # encoding: ascii-8bit
2
2
 
3
- # Copyright 2022 OpenC3, Inc.
3
+ # Copyright 2024 OpenC3, Inc.
4
4
  # All Rights Reserved.
5
5
  #
6
6
  # This program is free software; you can modify and/or redistribute it
@@ -14,7 +14,7 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2024, OpenC3, Inc.
17
+ # All changes Copyright 2025, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
20
  # This file may also be used under the terms of a commercial license
@@ -24,6 +24,7 @@
24
24
  # See https://github.com/OpenC3/cosmos/pull/1963
25
25
 
26
26
  require 'openc3/api/interface_api'
27
+ require 'openc3/models/queue_model'
27
28
  require 'openc3/models/target_model'
28
29
  require 'openc3/topics/command_topic'
29
30
  require 'openc3/topics/command_decom_topic'
@@ -452,7 +453,7 @@ module OpenC3
452
453
  end
453
454
 
454
455
  # NOTE: When adding new keywords to this method, make sure to update script/commands.rb
455
- def _cmd_implementation(method_name, *args, range_check:, hazardous_check:, raw:, timeout: nil, log_message: nil, manual: false, validate: true,
456
+ def _cmd_implementation(method_name, *args, range_check:, hazardous_check:, raw:, timeout: nil, log_message: nil, manual: false, validate: true, queue: nil,
456
457
  scope: $openc3_scope, token: $openc3_token, **kwargs)
457
458
  extract_string_kwargs_to_args(args, kwargs)
458
459
  unless [nil, true, false].include?(log_message)
@@ -538,9 +539,19 @@ module OpenC3
538
539
  'log_message' => log_message.to_s,
539
540
  'obfuscated_items' => packet['obfuscated_items'].to_s
540
541
  }
541
- CommandTopic.send_command(command, timeout: timeout, scope: scope)
542
+ # Users have to explicitly opt into a default queue by setting the OPENC3_DEFAULT_QUEUE
543
+ # At which point ALL commands will go to that queue unless they specifically opt out with queue: false
544
+ if ENV['OPENC3_DEFAULT_QUEUE'] && queue.nil?
545
+ queue = ENV['OPENC3_DEFAULT_QUEUE']
546
+ end
547
+ if queue
548
+ # Pull the command out of the script string, e.g. cmd("INST ABORT")
549
+ queued = cmd_string.split('("')[1].split('")')[0]
550
+ QueueModel.queue_command(queue, command: queued, username: username, scope: scope)
551
+ else
552
+ CommandTopic.send_command(command, timeout: timeout, scope: scope)
553
+ end
542
554
  return command
543
555
  end
544
-
545
556
  end
546
557
  end
@@ -31,6 +31,7 @@ rescue LoadError
31
31
  end
32
32
  require 'openc3/models/setting_model'
33
33
  require 'openc3/models/news_model'
34
+ require 'openc3/models/plugin_store_model'
34
35
 
35
36
  module OpenC3
36
37
  module Api
@@ -43,6 +44,7 @@ module OpenC3
43
44
  'set_setting',
44
45
  'save_setting', # DEPRECATED
45
46
  'update_news',
47
+ 'update_plugin_store',
46
48
  ])
47
49
 
48
50
  def list_settings(manual: false, scope: $openc3_scope, token: $openc3_token)
@@ -103,5 +105,11 @@ module OpenC3
103
105
  rescue Exception => e
104
106
  NewsModel.news_error("Error contacting OpenC3 news feed. #{e.message})")
105
107
  end
108
+
109
+ # Update the local copy of the plugin store data
110
+ def update_plugin_store(manual: false, scope: $openc3_scope, token: $openc3_token)
111
+ authorize(permission: 'admin', manual: manual, scope: scope, token: token)
112
+ PluginStoreModel.update()
113
+ end
106
114
  end
107
115
  end
@@ -1,6 +1,6 @@
1
1
  # encoding: ascii-8bit
2
2
 
3
- # Copyright 2022 OpenC3, Inc
3
+ # Copyright 2024 OpenC3, Inc
4
4
  # All Rights Reserved.
5
5
  #
6
6
  # This program is free software; you can modify and/or redistribute it
@@ -14,7 +14,7 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2022, OpenC3, Inc.
17
+ # All changes Copyright 2025, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
20
  # This file may also be used under the terms of a commercial license
@@ -22,7 +22,7 @@
22
22
  #
23
23
  # A portion of this file was funded by Blue Origin Enterprises, L.P.
24
24
  # See https://github.com/OpenC3/cosmos/pull/1963
25
-
25
+ #
26
26
  # A portion of this file was funded by Blue Origin Enterprises, L.P.
27
27
  # See https://github.com/OpenC3/cosmos/pull/1957
28
28
 
@@ -49,6 +49,7 @@ module OpenC3
49
49
  'normalize_tlm',
50
50
  'get_tlm_buffer',
51
51
  'get_tlm_packet',
52
+ 'get_tlm_available',
52
53
  'get_tlm_values',
53
54
  'get_all_tlm',
54
55
  'get_all_telemetry', # DEPRECATED
@@ -254,6 +255,81 @@ module OpenC3
254
255
  items.zip(current_values).map { | item , values | [item, values[0], values[1]]}
255
256
  end
256
257
 
258
+ # Returns the available items from a list of requested screen items
259
+ # This does the packet introspection to determine what is actually available
260
+ # Like if you ask for WITH_UNITS but only RAW is available
261
+ def get_tlm_available(items, manual: false, scope: $openc3_scope, token: $openc3_token)
262
+ results = []
263
+ items.each do |item|
264
+ item_upcase = item.to_s.upcase
265
+ target_name, orig_packet_name, item_name, value_type = item_upcase.split('__')
266
+ packet_name = orig_packet_name
267
+ raise ArgumentError, "items must be formatted as TGT__PKT__ITEM__TYPE" if target_name.nil? || packet_name.nil? || item_name.nil? || value_type.nil?
268
+ if orig_packet_name == 'LATEST'
269
+ # TODO: Do we need to lookup ALL the possible packets for this item?
270
+ # We can have a large cache_timeout of 1 because all we're trying to do is lookup a packet
271
+ packet_name = CvtModel.determine_latest_packet_for_item(target_name, item_name, cache_timeout: 1, scope: scope)
272
+ end
273
+ authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, scope: scope, token: token)
274
+ begin
275
+ item = TargetModel.packet_item(target_name, packet_name, item_name, scope: scope)
276
+ if Packet::RESERVED_ITEM_NAMES.include?(item_name)
277
+ value_type = 'RAW' # Must request the raw value when dealing with the reserved items
278
+ end
279
+
280
+ # QuestDB 9.0.0 only supports DOUBLE arrays: https://questdb.com/docs/concept/array/
281
+ if item['array_size']
282
+ # TODO: This needs work ... we're JSON encoding non numeric array values
283
+ if item['data_type'] == 'STRING' or item['data_type'] == 'BLOCK'
284
+ results << nil
285
+ next
286
+ end
287
+ value_type = 'RAW'
288
+ end
289
+
290
+ case value_type
291
+ when 'WITH_UNITS'
292
+ if item['units']
293
+ results << [target_name, orig_packet_name, item_name, 'WITH_UNITS'].join('__')
294
+ elsif item['format_string']
295
+ results << [target_name, orig_packet_name, item_name, 'FORMATTED'].join('__')
296
+ # This logic must match the logic in Packet#decom
297
+ elsif item['states'] or (item['read_conversion'] and item['data_type'] != 'DERIVED')
298
+ results << [target_name, orig_packet_name, item_name, 'CONVERTED'].join('__')
299
+ else
300
+ results << [target_name, orig_packet_name, item_name, 'RAW'].join('__')
301
+ end
302
+ when 'FORMATTED'
303
+ if item['format_string']
304
+ results << [target_name, orig_packet_name, item_name, 'FORMATTED'].join('__')
305
+ # This logic must match the logic in Packet#decom
306
+ elsif item['states'] or (item['read_conversion'] and item['data_type'] != 'DERIVED')
307
+ results << [target_name, orig_packet_name, item_name, 'CONVERTED'].join('__')
308
+ else
309
+ results << [target_name, orig_packet_name, item_name, 'RAW'].join('__')
310
+ end
311
+ when 'CONVERTED'
312
+ # This logic must match the logic in Packet#decom
313
+ if item['states'] or (item['read_conversion'] and item['data_type'] != 'DERIVED')
314
+ results << [target_name, orig_packet_name, item_name, 'CONVERTED'].join('__')
315
+ else
316
+ results << [target_name, orig_packet_name, item_name, 'RAW'].join('__')
317
+ end
318
+ else # RAW or unknown
319
+ results << [target_name, orig_packet_name, item_name, 'RAW'].join('__')
320
+ end
321
+
322
+ # Tack on __LIMITS to notify that we have an available limits value
323
+ if item['limits']['DEFAULT']
324
+ results[-1] += '__LIMITS'
325
+ end
326
+ rescue RuntimeError => e
327
+ results << nil
328
+ end
329
+ end
330
+ results
331
+ end
332
+
257
333
  # Returns all the item values (along with their limits state). The items
258
334
  # can be from any target and packet and thus must be fully qualified with
259
335
  # their target and packet names.
@@ -264,26 +340,32 @@ module OpenC3
264
340
  # @return [Array<Object, Symbol>]
265
341
  # Array consisting of the item value and limits state
266
342
  # given as symbols such as :RED, :YELLOW, :STALE
267
- def get_tlm_values(items, stale_time: 30, cache_timeout: nil, manual: false, scope: $openc3_scope, token: $openc3_token)
268
- if !items.is_a?(Array) || !items[0].is_a?(String)
343
+ def get_tlm_values(items, stale_time: 30, cache_timeout: nil, manual: false, start_time: nil, end_time: nil, scope: $openc3_scope, token: $openc3_token)
344
+ if !items.is_a?(Array)
269
345
  raise ArgumentError, "items must be array of strings: ['TGT__PKT__ITEM__TYPE', ...]"
270
346
  end
271
347
  packets = []
272
348
  cvt_items = []
273
349
  items.each_with_index do |item, index|
274
- item_upcase = item.to_s.upcase
275
- target_name, packet_name, item_name, value_type = item_upcase.split('__')
276
- raise ArgumentError, "items must be formatted as TGT__PKT__ITEM__TYPE" if target_name.nil? || packet_name.nil? || item_name.nil? || value_type.nil?
277
- packet_name = CvtModel.determine_latest_packet_for_item(target_name, item_name, cache_timeout: cache_timeout, scope: scope) if packet_name == 'LATEST'
278
- # Change packet_name in case of LATEST and ensure upcase
279
- cvt_items[index] = [target_name, packet_name, item_name, value_type]
350
+ if item.nil?
351
+ # null items mean that it doesn't exist
352
+ cvt_items[index] = nil
353
+ else
354
+ item_upcase = item.to_s.upcase
355
+ target_name, packet_name, item_name, value_type, limits = item_upcase.split('__')
356
+ raise ArgumentError, "items must be formatted as TGT__PKT__ITEM__TYPE" if target_name.nil? || packet_name.nil? || item_name.nil? || value_type.nil?
357
+ if packet_name == 'LATEST' # Lookup packet_name in case of LATEST
358
+ packet_name = CvtModel.determine_latest_packet_for_item(target_name, item_name, cache_timeout: cache_timeout, scope: scope)
359
+ end
360
+ end
361
+ cvt_items[index] = [target_name, packet_name, item_name, value_type, limits]
280
362
  packets << [target_name, packet_name]
281
363
  end
282
364
  packets.uniq!
283
365
  packets.each do |target_name, packet_name|
284
366
  authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, manual: manual, scope: scope, token: token)
285
367
  end
286
- CvtModel.get_tlm_values(cvt_items, stale_time: stale_time, cache_timeout: cache_timeout, scope: scope)
368
+ CvtModel.get_tlm_values(cvt_items, stale_time: stale_time, cache_timeout: cache_timeout, start_time: start_time, end_time: end_time, scope: scope)
287
369
  end
288
370
 
289
371
  # Returns an array of all the telemetry packet hashes
@@ -374,16 +456,16 @@ module OpenC3
374
456
  raise ArgumentError, "packets must be nested array: [['TGT','PKT'],...]"
375
457
  end
376
458
 
377
- result = {}
459
+ results = {}
378
460
  packets.each do |target_name, packet_name|
379
461
  target_name = target_name.upcase
380
462
  packet_name = packet_name.upcase
381
463
  authorize(permission: 'tlm', target_name: target_name, packet_name: packet_name, manual: manual, scope: scope, token: token)
382
464
  topic = "#{scope}__DECOM__{#{target_name}}__#{packet_name}"
383
465
  id, _ = Topic.get_newest_message(topic)
384
- result[topic] = id ? id : '0-0'
466
+ results[topic] = id ? id : '0-0'
385
467
  end
386
- result.to_a.join(SUBSCRIPTION_DELIMITER)
468
+ results.to_a.join(SUBSCRIPTION_DELIMITER)
387
469
  end
388
470
  # Alias the singular as well since that matches COSMOS 4
389
471
  alias subscribe_packet subscribe_packets
@@ -14,10 +14,10 @@
14
14
  # GNU Affero General Public License for more details.
15
15
 
16
16
  # Modified by OpenC3, Inc.
17
- # All changes Copyright 2022, OpenC3, Inc.
17
+ # All changes Copyright 2025, OpenC3, Inc.
18
18
  # All Rights Reserved
19
19
  #
20
- # This file may also be used under the terms of a commercial license
20
+ # This file may also be used under the terms of a commercial license
21
21
  # if purchased from OpenC3, Inc.
22
22
 
23
23
  # OpenC3 specific additions to the Ruby Kernel module
@@ -40,6 +40,6 @@ module Kernel
40
40
  # @param start [Integer] The number of stack entries to skip
41
41
  # @return [Symbol] The name of the calling method
42
42
  def calling_method(start = 1)
43
- caller[start][/`([^']*)'/, 1].intern
43
+ caller[start][/[`']([^']*)'/, 1].intern
44
44
  end
45
45
  end
@@ -319,19 +319,23 @@ module OpenC3
319
319
  begin
320
320
  @file.close unless @file.closed?
321
321
  Logger.debug "Log File Closed : #{@filename}"
322
- date = first_timestamp[0..7] # YYYYMMDD
323
- bucket_key = File.join(@remote_log_directory, date, bucket_filename())
324
- # Cleanup timestamps here so they are unset for the next file
325
- @first_time = nil
326
- @last_time = nil
327
- threads << BucketUtilities.move_log_file_to_bucket(@filename, bucket_key)
328
- # Now that the file is in storage, trim the Redis stream after a delay
329
- @cleanup_offsets << {}
330
- @last_offsets.each do |redis_topic, last_offset|
331
- @cleanup_offsets[-1][redis_topic] = last_offset
322
+ # Only try to moce the file if we've written data to it
323
+ # This is indicated by the first and last timestamps being set
324
+ if @first_time and @last_time
325
+ date = first_timestamp[0..7] # YYYYMMDD
326
+ bucket_key = File.join(@remote_log_directory, date, bucket_filename())
327
+ # Cleanup timestamps here so they are unset for the next file
328
+ @first_time = nil
329
+ @last_time = nil
330
+ threads << BucketUtilities.move_log_file_to_bucket(@filename, bucket_key)
331
+ # Now that the file is in storage, trim the Redis stream after a delay
332
+ @cleanup_offsets << {}
333
+ @last_offsets.each do |redis_topic, last_offset|
334
+ @cleanup_offsets[-1][redis_topic] = last_offset
335
+ end
336
+ @cleanup_times << (Time.now + CLEANUP_DELAY)
337
+ @last_offsets.clear
332
338
  end
333
- @cleanup_times << (Time.now + CLEANUP_DELAY)
334
- @last_offsets.clear
335
339
  rescue Exception => e
336
340
  Logger.error "Error closing #{@filename} : #{e.formatted}"
337
341
  end
@@ -298,7 +298,7 @@ module OpenC3
298
298
  @interface.write(command)
299
299
 
300
300
  command.obfuscate
301
-
301
+
302
302
  if command.validator and validate
303
303
  begin
304
304
  result, reason = command.validator.post_check(command)
@@ -559,6 +559,19 @@ module OpenC3
559
559
  target.interface = new_interface
560
560
  end
561
561
  @interface = new_interface
562
+
563
+ # Update the model
564
+ if @interface_or_router == 'INTERFACE'
565
+ interface_model = InterfaceModel.get(name: @interface.name, scope: @scope)
566
+ # config_params[0] is the filename so set the rest
567
+ interface_model['config_params'][1..-1] = *params
568
+ InterfaceModel.set(interface_model, scope: @scope)
569
+ else
570
+ router_model = RouterModel.get(name: @interface.name, scope: @scope)
571
+ # config_params[0] is the filename so set the rest
572
+ router_model['config_params'][1..-1] = *params
573
+ RouterModel.set(router_model, scope: @scope)
574
+ end
562
575
  end
563
576
 
564
577
  @interface.state = 'ATTEMPTING'
@@ -43,8 +43,8 @@ module OpenC3
43
43
  # Fortify: Process Control
44
44
  # This is dangerous! However, plugins need to be able to run whatever they want.
45
45
  # Only admins can install plugins and they need to be vetted for content.
46
- # NOTE: In OpenC3 EE each microservice gets its own container so the potential
47
- # footprint is much smaller. In OpenSource OpenC3 you're in the same container
46
+ # NOTE: In Enterprise each microservice gets its own container so the potential
47
+ # footprint is much smaller. In Core you're in the same container
48
48
  # as all the other plugins.
49
49
  exec(*@config["cmd"])
50
50
  end