openc3 6.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 35ed91c6cd987b0a1c139ef5cd9670d204eb524ae7eebdf971d58aef3dbc5faa
4
- data.tar.gz: 1a0d8ca81f317a7a5227d56be83ab847bed1a7c4ac48ea991f34ed46a3e80e1d
3
+ metadata.gz: 0b3a2017659bdea9209911793ce6d8f53c28320df77e9c938e1a3ec6d0e88527
4
+ data.tar.gz: 950e4c86f740f303186262aa4c808f063219b7097e4b1a0c343aa83da9a71b48
5
5
  SHA512:
6
- metadata.gz: f305acecd79bfd96b93829a7a3b67b4095ef45104fa7571f5d3c59ac70e753668f0a8e5bde7ad2818c3c22966bf55b524e1fa6bb41a14aa124a2c1f063115781
7
- data.tar.gz: 85f4fc3479388969c39cdedcd180498cf1eeb3870d0c609590f4fb6c816d25efdbc02aff4cf499a96da88b3176846a6ba7e08e81dca483659928c475f673be36
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'
@@ -1015,6 +1016,10 @@ if not ARGV[0].nil? # argument(s) given
1015
1016
  end
1016
1017
  end
1017
1018
 
1019
+ when 'createqueue'
1020
+ queue = OpenC3::QueueModel.new(name: ARGV[1], state: 'RELEASE', scope: ARGV[2])
1021
+ queue.create
1022
+
1018
1023
  when 'destroyscope'
1019
1024
  scope = OpenC3::ScopeModel.get_model(name: ARGV[1])
1020
1025
  scope.destroy
@@ -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
@@ -293,7 +293,8 @@ module OpenC3
293
293
  results << [target_name, orig_packet_name, item_name, 'WITH_UNITS'].join('__')
294
294
  elsif item['format_string']
295
295
  results << [target_name, orig_packet_name, item_name, 'FORMATTED'].join('__')
296
- elsif item['read_conversion'] or item['states']
296
+ # This logic must match the logic in Packet#decom
297
+ elsif item['states'] or (item['read_conversion'] and item['data_type'] != 'DERIVED')
297
298
  results << [target_name, orig_packet_name, item_name, 'CONVERTED'].join('__')
298
299
  else
299
300
  results << [target_name, orig_packet_name, item_name, 'RAW'].join('__')
@@ -301,13 +302,15 @@ module OpenC3
301
302
  when 'FORMATTED'
302
303
  if item['format_string']
303
304
  results << [target_name, orig_packet_name, item_name, 'FORMATTED'].join('__')
304
- elsif item['read_conversion'] or item['states']
305
+ # This logic must match the logic in Packet#decom
306
+ elsif item['states'] or (item['read_conversion'] and item['data_type'] != 'DERIVED')
305
307
  results << [target_name, orig_packet_name, item_name, 'CONVERTED'].join('__')
306
308
  else
307
309
  results << [target_name, orig_packet_name, item_name, 'RAW'].join('__')
308
310
  end
309
311
  when 'CONVERTED'
310
- if item['read_conversion'] or item['states']
312
+ # This logic must match the logic in Packet#decom
313
+ if item['states'] or (item['read_conversion'] and item['data_type'] != 'DERIVED')
311
314
  results << [target_name, orig_packet_name, item_name, 'CONVERTED'].join('__')
312
315
  else
313
316
  results << [target_name, orig_packet_name, item_name, 'RAW'].join('__')
@@ -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'
@@ -0,0 +1,166 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2025 OpenC3, Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # This program is free software; you can modify and/or redistribute it
7
+ # under the terms of the GNU Affero General Public License
8
+ # as published by the Free Software Foundation; version 3 with
9
+ # attribution addendums as found in the LICENSE.txt
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Affero General Public License for more details.
15
+ #
16
+ # This file may also be used under the terms of a commercial license
17
+ # if purchased from OpenC3, Inc.
18
+
19
+ require 'openc3/microservices/microservice'
20
+ require 'openc3/topics/queue_topic'
21
+ require 'openc3/utilities/authentication'
22
+ require 'openc3/script'
23
+
24
+ module OpenC3
25
+ # The queue processor runs in a single thread and processes commands via cmd_api.
26
+ class QueueProcessor
27
+ attr_accessor :state
28
+ attr_reader :name, :scope
29
+
30
+ def initialize(name:, state:, logger:, scope:)
31
+ @name = name
32
+ @logger = logger
33
+ @scope = scope
34
+ @state = state
35
+ @cancel_thread = false
36
+ end
37
+
38
+ def get_token(username)
39
+ if ENV['OPENC3_API_CLIENT'].nil?
40
+ ENV['OPENC3_API_PASSWORD'] ||= ENV['OPENC3_SERVICE_PASSWORD']
41
+ return OpenC3Authentication.new().token
42
+ else
43
+ # Check for offline access token
44
+ model = nil
45
+ model = OpenC3::OfflineAccessModel.get_model(name: username, scope: @scope) if username and username != ''
46
+ if model and model.offline_access_token
47
+ auth = OpenC3KeycloakAuthentication.new(ENV['OPENC3_KEYCLOAK_URL'])
48
+ return auth.get_token_from_refresh_token(model.offline_access_token)
49
+ else
50
+ return nil
51
+ end
52
+ end
53
+ end
54
+
55
+ def run
56
+ while true
57
+ if @state == 'RELEASE'
58
+ process_queued_commands()
59
+ else
60
+ sleep 0.2
61
+ end
62
+ break if @cancel_thread
63
+ end
64
+ end
65
+
66
+ def process_queued_commands
67
+ while @state == 'RELEASE'
68
+ begin
69
+ _queue_name, command_data, _timestamp = Store.bzpopmin("#{@scope}:#{@name}", timeout: 0.2)
70
+ if command_data
71
+ command = JSON.parse(command_data)
72
+ username = command['username']
73
+ token = get_token(username)
74
+ # It's important to set queue: false here to avoid infinite recursion when
75
+ # OPENC3_DEFAULT_QUEUE is set because commands would be re-queued to the default queue
76
+ cmd_no_hazardous_check(command['value'], queue: false, scope: @scope, token: token)
77
+ end
78
+ rescue StandardError => e
79
+ @logger.error "QueueProcessor failed to process command from queue #{@name}\n#{e.message}"
80
+ end
81
+ break if @cancel_thread
82
+ end
83
+ end
84
+
85
+ def shutdown
86
+ @cancel_thread = true
87
+ end
88
+ end
89
+
90
+ # The queue microservice starts a processor then gets the queue entries from redis.
91
+ # It then monitors the QueueTopic for changes.
92
+ class QueueMicroservice < Microservice
93
+ attr_reader :name, :processor, :processor_thread
94
+
95
+ def initialize(*args)
96
+ super(*args)
97
+ @queue_name = @name.split('__')[2]
98
+
99
+ initial_state = 'HOLD'
100
+ (@config['options'] || []).each do |option|
101
+ case option[0].upcase
102
+ when 'QUEUE_STATE'
103
+ initial_state = option[1]
104
+ else
105
+ @logger.error("Unknown option passed to microservice #{@name}: #{option}")
106
+ end
107
+ end
108
+
109
+ @processor = QueueProcessor.new(name: @queue_name, state: initial_state, logger: @logger, scope: @scope)
110
+ @processor_thread = nil
111
+ @read_topic = true
112
+ end
113
+
114
+ def run
115
+ @logger.info "QueueMicroservice running"
116
+ @processor_thread = Thread.new { @processor.run }
117
+
118
+ # Let the frontend know that the microservice has been deployed and is running
119
+ notification = {
120
+ 'kind' => 'deployed',
121
+ # name and updated_at fields are required for Event formatting
122
+ 'data' => JSON.generate({
123
+ 'name' => @name,
124
+ 'updated_at' => Time.now.to_nsec_from_epoch,
125
+ }),
126
+ }
127
+ QueueTopic.write_notification(notification, scope: @scope)
128
+
129
+ loop do
130
+ break if @cancel_thread
131
+ block_for_updates()
132
+ end
133
+ @processor.shutdown()
134
+ @processor_thread.join() if @processor_thread
135
+ @logger.info "QueueMicroservice exiting"
136
+ end
137
+
138
+ def block_for_updates
139
+ @read_topic = true
140
+ while @read_topic && !@cancel_thread
141
+ begin
142
+ QueueTopic.read_topics(@topics) do |_topic, _msg_id, msg_hash, _redis|
143
+ data = JSON.parse(msg_hash['data']) if msg_hash['data']
144
+ if data['name'] == @queue_name and msg_hash['kind'] == 'updated'
145
+ @processor.state = data['state']
146
+ end
147
+ end
148
+ rescue StandardError => e
149
+ @logger.error "QueueMicroservice failed to read topics #{@topics}\n#{e.formatted}"
150
+ end
151
+ end
152
+ end
153
+
154
+ def shutdown
155
+ @read_topic = false
156
+ @processor.shutdown() if @processor
157
+ super
158
+ end
159
+ end
160
+ end
161
+
162
+ if __FILE__ == $0
163
+ OpenC3::QueueMicroservice.run
164
+ OpenC3::ThreadManager.instance.shutdown
165
+ OpenC3::ThreadManager.instance.join
166
+ end
@@ -0,0 +1,232 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2025 OpenC3, Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # This program is free software; you can modify and/or redistribute it
7
+ # under the terms of the GNU Affero General Public License
8
+ # as published by the Free Software Foundation; version 3 with
9
+ # attribution addendums as found in the LICENSE.txt
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Affero General Public License for more details.
15
+
16
+ # This file may also be used under the terms of a commercial license
17
+ # if purchased from OpenC3, Inc.
18
+
19
+ require 'openc3/models/model'
20
+ require 'openc3/models/microservice_model'
21
+ require 'openc3/topics/queue_topic'
22
+ require 'openc3/utilities/logger'
23
+
24
+ module OpenC3
25
+ class QueueError < StandardError; end
26
+
27
+ class QueueModel < Model
28
+ PRIMARY_KEY = 'openc3__queue'.freeze
29
+
30
+ @@class_mutex = Mutex.new
31
+
32
+ # NOTE: The following three class methods are used by the ModelController
33
+ # and are reimplemented to enable various Model class methods to work
34
+ def self.get(name:, scope:)
35
+ super("#{scope}__#{PRIMARY_KEY}", name: name)
36
+ end
37
+
38
+ def self.names(scope:)
39
+ super("#{scope}__#{PRIMARY_KEY}")
40
+ end
41
+
42
+ def self.all(scope:)
43
+ super("#{scope}__#{PRIMARY_KEY}")
44
+ end
45
+ # END NOTE
46
+
47
+ def self.queue_command(name, command:, username:, scope:)
48
+ model = get_model(name: name, scope: scope)
49
+ raise QueueError, "Queue '#{name}' not found in scope '#{scope}'" unless model
50
+
51
+ if model.state != 'DISABLE'
52
+ result = Store.zrevrange("#{scope}:#{name}", 0, 0, with_scores: true)
53
+ if result.empty?
54
+ index = 1.0
55
+ else
56
+ index = result[0][1].to_f + 1
57
+ end
58
+ Store.zadd("#{scope}:#{name}", index, { username: username, value: command, timestamp: Time.now.to_nsec_from_epoch }.to_json)
59
+ model.notify(kind: 'command')
60
+ else
61
+ raise QueueError, "Queue '#{name}' is disabled. Command '#{command}' not queued."
62
+ end
63
+ end
64
+
65
+ attr_accessor :name, :state
66
+
67
+ def initialize(name:, scope:, state: 'HOLD', updated_at: nil)
68
+ super("#{scope}__#{PRIMARY_KEY}", name: name, updated_at: updated_at, scope: scope)
69
+ @microservice_name = "#{scope}__QUEUE__#{name}"
70
+ if %w(HOLD RELEASE DISABLE).include?(state)
71
+ @state = state
72
+ else
73
+ @state = 'HOLD'
74
+ end
75
+ end
76
+
77
+ def create(update: false, force: false, queued: false)
78
+ super(update: update, force: force, queued: queued)
79
+ if update
80
+ notify(kind: 'updated')
81
+ else
82
+ deploy()
83
+ notify(kind: 'created')
84
+ end
85
+ end
86
+
87
+ # @return [Hash] generated from the QueueModel
88
+ def as_json(*a)
89
+ return {
90
+ 'name' => @name,
91
+ 'scope' => @scope,
92
+ 'state' => @state,
93
+ 'updated_at' => @updated_at
94
+ }
95
+ end
96
+
97
+ # @return [] update the redis stream / queue topic that something has changed
98
+ def notify(kind:)
99
+ notification = {
100
+ 'kind' => kind,
101
+ 'data' => JSON.generate(as_json(:allow_nan => true)),
102
+ }
103
+ QueueTopic.write_notification(notification, scope: @scope)
104
+ end
105
+
106
+ def insert_command(index, command_data)
107
+ if @state == 'DISABLE'
108
+ raise QueueError, "Queue '#{@name}' is disabled. Command '#{command_data['value']}' not queued."
109
+ end
110
+
111
+ unless index
112
+ result = Store.zrevrange("#{@scope}:#{@name}", 0, 0, with_scores: true)
113
+ if result.empty?
114
+ index = 1.0
115
+ else
116
+ index = result[0][1].to_f + 1
117
+ end
118
+ end
119
+ Store.zadd("#{@scope}:#{@name}", index, command_data.to_json)
120
+ notify(kind: 'command')
121
+ end
122
+
123
+ def update_command(index:, command:, username:)
124
+ if @state == 'DISABLE'
125
+ raise QueueError, "Queue '#{@name}' is disabled. Command at index #{index} not updated."
126
+ end
127
+
128
+ # Check if command exists at the given index
129
+ existing = Store.zrangebyscore("#{@scope}:#{@name}", index, index)
130
+ if existing.empty?
131
+ raise QueueError, "No command found at index #{index} in queue '#{@name}'"
132
+ end
133
+
134
+ # Remove the existing command and add the new one at the same index
135
+ Store.zremrangebyscore("#{@scope}:#{@name}", index, index)
136
+ command_data = { username: username, value: command, timestamp: Time.now.to_nsec_from_epoch }
137
+ Store.zadd("#{@scope}:#{@name}", index, command_data.to_json)
138
+ notify(kind: 'command')
139
+ end
140
+
141
+ def remove_command(index = nil)
142
+ if @state == 'DISABLE'
143
+ raise QueueError, "Queue '#{@name}' is disabled. Command not removed."
144
+ end
145
+
146
+ if index
147
+ # Remove specific index
148
+ result = Store.zrangebyscore("#{@scope}:#{@name}", index, index)
149
+ if result.empty?
150
+ return nil
151
+ else
152
+ Store.zremrangebyscore("#{@scope}:#{@name}", index, index)
153
+ command_data = JSON.parse(result[0])
154
+ command_data['index'] = index.to_f
155
+ notify(kind: 'command')
156
+ return command_data
157
+ end
158
+ else
159
+ # Remove first element (lowest score)
160
+ result = Store.zrange("#{@scope}:#{@name}", 0, 0, with_scores: true)
161
+ if result.empty?
162
+ return nil
163
+ else
164
+ score = result[0][1]
165
+ Store.zremrangebyscore("#{@scope}:#{@name}", score, score)
166
+ command_data = JSON.parse(result[0][0])
167
+ command_data['index'] = score.to_f
168
+ notify(kind: 'command')
169
+ return command_data
170
+ end
171
+ end
172
+ end
173
+
174
+ def list
175
+ return Store.zrange("#{@scope}:#{@name}", 0, -1, with_scores: true).map do |item|
176
+ result = JSON.parse(item[0])
177
+ result['index'] = item[1].to_f
178
+ result
179
+ end
180
+ end
181
+
182
+ def create_microservice(topics:)
183
+ # queue Microservice
184
+ microservice = MicroserviceModel.new(
185
+ name: @microservice_name,
186
+ folder_name: nil,
187
+ cmd: ['ruby', 'queue_microservice.rb', @microservice_name],
188
+ work_dir: '/openc3/lib/openc3/microservices',
189
+ options: [
190
+ ["QUEUE_STATE", @state],
191
+ ],
192
+ topics: topics,
193
+ target_names: [],
194
+ plugin: nil,
195
+ scope: @scope
196
+ )
197
+ microservice.create
198
+ end
199
+
200
+ def deploy
201
+ topics = ["#{@scope}__#{QueueTopic::PRIMARY_KEY}"]
202
+ if MicroserviceModel.get_model(name: @microservice_name, scope: @scope).nil?
203
+ create_microservice(topics: topics)
204
+ end
205
+ end
206
+
207
+ def undeploy
208
+ model = MicroserviceModel.get_model(name: @microservice_name, scope: @scope)
209
+ if model
210
+ # Let the frontend know that the microservice is shutting down
211
+ # Custom event which matches the 'deployed' event in QueueMicroservice
212
+ notification = {
213
+ 'kind' => 'undeployed',
214
+ # name and updated_at fields are required for Event formatting
215
+ 'data' => JSON.generate({
216
+ 'name' => @microservice_name,
217
+ 'updated_at' => Time.now.to_nsec_from_epoch,
218
+ }),
219
+ }
220
+ QueueTopic.write_notification(notification, scope: @scope)
221
+ model.destroy
222
+ end
223
+ end
224
+
225
+ # Delete the model from the Store
226
+ def destroy
227
+ undeploy()
228
+ Store.zremrangebyrank("#{@scope}:#{@name}", 0, -1)
229
+ super()
230
+ end
231
+ end
232
+ end
@@ -122,7 +122,7 @@ module OpenC3
122
122
  # except for range_check, hazardous_check, and raw as they are part of the cmd name
123
123
  # manual is always false since this is called from script and that is the default
124
124
  # NOTE: This is a helper method and should not be called directly
125
- def _cmd(cmd, cmd_no_hazardous, *args, timeout: nil, log_message: nil, validate: true, scope: $openc3_scope, token: $openc3_token, **kwargs)
125
+ def _cmd(cmd, cmd_no_hazardous, *args, timeout: nil, log_message: nil, validate: true, queue: nil, scope: $openc3_scope, token: $openc3_token, **kwargs)
126
126
  extract_string_kwargs_to_args(args, kwargs)
127
127
  raw = cmd.include?('raw')
128
128
  no_range = cmd.include?('no_range') || cmd.include?('no_checks')
@@ -132,7 +132,7 @@ module OpenC3
132
132
  else
133
133
  begin
134
134
  begin
135
- command = $api_server.method_missing(cmd, *args, timeout: timeout, log_message: log_message, validate: validate, scope: scope, token: token)
135
+ command = $api_server.method_missing(cmd, *args, timeout: timeout, log_message: log_message, validate: validate, queue: queue, scope: scope, token: token)
136
136
  if log_message.nil? or log_message
137
137
  _log_cmd(command, raw, no_range, no_hazardous)
138
138
  end
@@ -140,7 +140,7 @@ module OpenC3
140
140
  # This opens a prompt at which point they can cancel and stop the script
141
141
  # or say Yes and send the command. Thus we don't care about the return value.
142
142
  prompt_for_hazardous(e.target_name, e.cmd_name, e.hazardous_description)
143
- command = $api_server.method_missing(cmd_no_hazardous, *args, timeout: timeout, log_message: log_message, validate: validate, scope: scope, token: token)
143
+ command = $api_server.method_missing(cmd_no_hazardous, *args, timeout: timeout, log_message: log_message, validate: validate, queue: queue, scope: scope, token: token)
144
144
  if log_message.nil? or log_message
145
145
  _log_cmd(command, raw, no_range, no_hazardous)
146
146
  end
@@ -0,0 +1,80 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2025 OpenC3, Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # This program is free software; you can modify and/or redistribute it
7
+ # under the terms of the GNU Affero General Public License
8
+ # as published by the Free Software Foundation; version 3 with
9
+ # attribution addendums as found in the LICENSE.txt
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Affero General Public License for more details.
15
+
16
+ # This file may also be used under the terms of a commercial license
17
+ # if purchased from OpenC3, Inc.
18
+
19
+ require 'openc3/script/extract'
20
+
21
+ module OpenC3
22
+ module Script
23
+ include Extract
24
+
25
+ private
26
+
27
+ # Helper method that makes the request and parses the response
28
+ def _make_request(action:, verb:, uri:, scope:, data: nil)
29
+ response = $api_server.request(verb, uri, data: data, json: data.nil? ? false : true, scope: scope)
30
+ if response.nil?
31
+ raise "Failed to #{action}. No response from server."
32
+ elsif response.status != 200 and response.status != 201
33
+ result = JSON.parse(response.body, :allow_nan => true, :create_additions => true)
34
+ raise "Failed to #{action} due to #{result['message']}"
35
+ end
36
+ return JSON.parse(response.body, :allow_nan => true, :create_additions => true)
37
+ end
38
+
39
+ def queue_all(scope: $openc3_scope)
40
+ return _make_request(action: 'index queue', verb: 'get', uri: "/openc3-api/queues", scope: scope)
41
+ end
42
+
43
+ def queue_get(name, scope: $openc3_scope)
44
+ return _make_request(action: 'get queue', verb: 'get', uri: "/openc3-api/queues/#{name}", scope: scope)
45
+ end
46
+
47
+ def queue_list(name, scope: $openc3_scope)
48
+ return _make_request(action: 'list queue', verb: 'get', uri: "/openc3-api/queues/#{name}/list", scope: scope)
49
+ end
50
+
51
+ def queue_create(name, state: 'HOLD', scope: $openc3_scope)
52
+ data = {}
53
+ data['state'] = state
54
+ return _make_request(action: 'create queue', verb: 'post', uri: "/openc3-api/queues/#{name}", data: data, scope: scope)
55
+ end
56
+
57
+ def queue_hold(name, scope: $openc3_scope)
58
+ return _make_request(action: 'hold queue', verb: 'post', uri: "/openc3-api/queues/#{name}/hold", scope: scope)
59
+ end
60
+
61
+ def queue_release(name, scope: $openc3_scope)
62
+ return _make_request(action: 'release queue', verb: 'post', uri: "/openc3-api/queues/#{name}/release", scope: scope)
63
+ end
64
+
65
+ def queue_disable(name, scope: $openc3_scope)
66
+ return _make_request(action: 'disable queue', verb: 'post', uri: "/openc3-api/queues/#{name}/disable", scope: scope)
67
+ end
68
+
69
+ def queue_exec(name, index: nil, scope: $openc3_scope)
70
+ data = {}
71
+ data['index'] = index if index
72
+ return _make_request(action: 'exec command', verb: 'post', uri: "/openc3-api/queues/#{name}/exec_command", data: data, scope: scope)
73
+ end
74
+
75
+ def queue_delete(name, scope: $openc3_scope)
76
+ return _make_request(action: 'delete queue', verb: 'delete', uri: "/openc3-api/queues/#{name}", scope: scope)
77
+ end
78
+ alias queue_destroy queue_delete
79
+ end
80
+ end
@@ -34,6 +34,7 @@ require 'openc3/script/limits'
34
34
  require 'openc3/script/metadata'
35
35
  require 'openc3/script/packages'
36
36
  require 'openc3/script/plugins'
37
+ require 'openc3/script/queue'
37
38
  require 'openc3/script/screen'
38
39
  require 'openc3/script/script_runner'
39
40
  require 'openc3/script/storage'
@@ -306,6 +306,17 @@ module OpenC3
306
306
  end
307
307
  end
308
308
 
309
+ # Queue WebSocket
310
+ class QueueEventsWebSocketApi < CmdTlmWebSocketApi
311
+ def initialize(history_count: 0, url: nil, write_timeout: 10.0, read_timeout: 10.0, connect_timeout: 5.0, authentication: nil, scope: $openc3_scope)
312
+ @identifier = {
313
+ channel: "QueueEventsChannel",
314
+ history_count: history_count
315
+ }
316
+ super(url: url, write_timeout: write_timeout, read_timeout: read_timeout, connect_timeout: connect_timeout, authentication: authentication, scope: scope)
317
+ end
318
+ end
319
+
309
320
  # Streaming API WebSocket
310
321
  class StreamingWebSocketApi < CmdTlmWebSocketApi
311
322
  def initialize(url: nil, write_timeout: 10.0, read_timeout: 10.0, connect_timeout: 5.0, authentication: nil, scope: $openc3_scope)
@@ -0,0 +1,29 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2025 OpenC3, Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # This program is free software; you can modify and/or redistribute it
7
+ # under the terms of the GNU Affero General Public License
8
+ # as published by the Free Software Foundation; version 3 with
9
+ # attribution addendums as found in the LICENSE.txt
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Affero General Public License for more details.
15
+
16
+ # This file may also be used under the terms of a commercial license
17
+ # if purchased from OpenC3, Inc.
18
+
19
+ require 'openc3/topics/topic'
20
+
21
+ module OpenC3
22
+ class QueueTopic < Topic
23
+ PRIMARY_KEY = "openc3_queue"
24
+
25
+ def self.write_notification(notification, scope:)
26
+ Topic.write_topic("#{scope}__#{PRIMARY_KEY}", notification, '*', 1000)
27
+ end
28
+ end
29
+ end
@@ -31,6 +31,7 @@ module OpenC3
31
31
  'openc3-cosmos-tool-admin',
32
32
  'openc3-cosmos-tool-bucketexplorer',
33
33
  'openc3-cosmos-tool-cmdsender',
34
+ 'openc3-cosmos-tool-cmdqueue',
34
35
  'openc3-cosmos-tool-cmdhistory',
35
36
  'openc3-cosmos-tool-cmdtlmserver',
36
37
  'openc3-cosmos-tool-dataextractor',
@@ -50,6 +51,7 @@ module OpenC3
50
51
  'openc3-cosmos-tool-calendar',
51
52
  'openc3-cosmos-tool-grafana',
52
53
  'openc3-cosmos-tool-systemhealth',
54
+ 'openc3-cosmos-tool-logexplorer',
53
55
  'openc3-tool-base',
54
56
  ]
55
57
 
@@ -1,14 +1,14 @@
1
1
  # encoding: ascii-8bit
2
2
 
3
- OPENC3_VERSION = '6.7.0'
3
+ OPENC3_VERSION = '6.8.0'
4
4
  module OpenC3
5
5
  module Version
6
6
  MAJOR = '6'
7
- MINOR = '7'
7
+ MINOR = '8'
8
8
  PATCH = '0'
9
9
  OTHER = ''
10
- BUILD = '6c4726a21dd004109cf840f83a99f5a8614cfbe3'
10
+ BUILD = '35fc05d093679cc901fc15e31a9af4b71fad61b4'
11
11
  end
12
- VERSION = '6.7.0'
13
- GEM_VERSION = '6.7.0'
12
+ VERSION = '6.8.0'
13
+ GEM_VERSION = '6.8.0'
14
14
  end
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "<%= tool_name %>",
3
- "version": "6.7.0",
3
+ "version": "6.8.0",
4
4
  "scripts": {
5
5
  "ng": "ng",
6
6
  "start": "ng serve",
@@ -23,7 +23,7 @@
23
23
  "@angular/platform-browser-dynamic": "^18.2.6",
24
24
  "@angular/router": "^18.2.6",
25
25
  "@astrouxds/astro-web-components": "^7.24.0",
26
- "@openc3/js-common": "6.7.0",
26
+ "@openc3/js-common": "6.8.0",
27
27
  "rxjs": "~7.8.0",
28
28
  "single-spa": "^5.9.5",
29
29
  "single-spa-angular": "^9.2.0",
@@ -16,7 +16,7 @@
16
16
  "@emotion/react": "^11.13.3",
17
17
  "@emotion/styled": "^11.11.0",
18
18
  "@mui/material": "^6.1.1",
19
- "@openc3/js-common": "6.7.0",
19
+ "@openc3/js-common": "6.8.0",
20
20
  "react": "^18.2.0",
21
21
  "react-dom": "^18.2.0",
22
22
  "single-spa-react": "^5.1.4"
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@astrouxds/astro-web-components": "^7.24.0",
15
- "@openc3/js-common": "6.7.0",
15
+ "@openc3/js-common": "6.8.0",
16
16
  "@smui/button": "^7.0.0",
17
17
  "@smui/common": "^7.0.0",
18
18
  "@smui/card": "^7.0.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "<%= tool_name %>",
3
- "version": "6.7.0",
3
+ "version": "6.8.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
@@ -11,8 +11,8 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@astrouxds/astro-web-components": "^7.24.0",
14
- "@openc3/js-common": "6.7.0",
15
- "@openc3/vue-common": "6.7.0",
14
+ "@openc3/js-common": "6.8.0",
15
+ "@openc3/vue-common": "6.8.0",
16
16
  "axios": "^1.7.7",
17
17
  "date-fns": "^4.1.0",
18
18
  "lodash": "^4.17.21",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "<%= widget_name %>",
3
- "version": "6.7.0",
3
+ "version": "6.8.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@astrouxds/astro-web-components": "^7.24.0",
11
- "@openc3/vue-common": "6.7.0",
11
+ "@openc3/vue-common": "6.8.0",
12
12
  "vuetify": "^3.7.1"
13
13
  },
14
14
  "devDependencies": {
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openc3
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.7.0
4
+ version: 6.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Melton
@@ -184,14 +184,14 @@ dependencies:
184
184
  requirements:
185
185
  - - "~>"
186
186
  - !ruby/object:Gem::Version
187
- version: '3.0'
187
+ version: '3.1'
188
188
  type: :runtime
189
189
  prerelease: false
190
190
  version_requirements: !ruby/object:Gem::Requirement
191
191
  requirements:
192
192
  - - "~>"
193
193
  - !ruby/object:Gem::Version
194
- version: '3.0'
194
+ version: '3.1'
195
195
  - !ruby/object:Gem::Dependency
196
196
  name: rackup
197
197
  requirement: !ruby/object:Gem::Requirement
@@ -800,28 +800,42 @@ dependencies:
800
800
  requirements:
801
801
  - - "~>"
802
802
  - !ruby/object:Gem::Version
803
- version: '0.21'
803
+ version: '0.22'
804
804
  type: :development
805
805
  prerelease: false
806
806
  version_requirements: !ruby/object:Gem::Requirement
807
807
  requirements:
808
808
  - - "~>"
809
809
  - !ruby/object:Gem::Version
810
- version: '0.21'
810
+ version: '0.22'
811
+ - !ruby/object:Gem::Dependency
812
+ name: rexml
813
+ requirement: !ruby/object:Gem::Requirement
814
+ requirements:
815
+ - - '='
816
+ - !ruby/object:Gem::Version
817
+ version: 3.4.1
818
+ type: :development
819
+ prerelease: false
820
+ version_requirements: !ruby/object:Gem::Requirement
821
+ requirements:
822
+ - - '='
823
+ - !ruby/object:Gem::Version
824
+ version: 3.4.1
811
825
  - !ruby/object:Gem::Dependency
812
826
  name: simplecov-cobertura
813
827
  requirement: !ruby/object:Gem::Requirement
814
828
  requirements:
815
829
  - - "~>"
816
830
  - !ruby/object:Gem::Version
817
- version: '2.1'
831
+ version: '3.0'
818
832
  type: :development
819
833
  prerelease: false
820
834
  version_requirements: !ruby/object:Gem::Requirement
821
835
  requirements:
822
836
  - - "~>"
823
837
  - !ruby/object:Gem::Version
824
- version: '2.1'
838
+ version: '3.0'
825
839
  description: |2
826
840
  OpenC3 provides all the functionality needed to send
827
841
  commands to and receive data from one or more embedded systems
@@ -1052,6 +1066,7 @@ files:
1052
1066
  - lib/openc3/microservices/multi_microservice.rb
1053
1067
  - lib/openc3/microservices/periodic_microservice.rb
1054
1068
  - lib/openc3/microservices/plugin_microservice.rb
1069
+ - lib/openc3/microservices/queue_microservice.rb
1055
1070
  - lib/openc3/microservices/reducer_microservice.rb
1056
1071
  - lib/openc3/microservices/router_microservice.rb
1057
1072
  - lib/openc3/microservices/scope_cleanup_microservice.rb
@@ -1087,6 +1102,7 @@ files:
1087
1102
  - lib/openc3/models/plugin_store_model.rb
1088
1103
  - lib/openc3/models/process_status_model.rb
1089
1104
  - lib/openc3/models/python_package_model.rb
1105
+ - lib/openc3/models/queue_model.rb
1090
1106
  - lib/openc3/models/reaction_model.rb
1091
1107
  - lib/openc3/models/reducer_model.rb
1092
1108
  - lib/openc3/models/router_model.rb
@@ -1145,6 +1161,7 @@ files:
1145
1161
  - lib/openc3/script/metadata.rb
1146
1162
  - lib/openc3/script/packages.rb
1147
1163
  - lib/openc3/script/plugins.rb
1164
+ - lib/openc3/script/queue.rb
1148
1165
  - lib/openc3/script/screen.rb
1149
1166
  - lib/openc3/script/script.rb
1150
1167
  - lib/openc3/script/script_runner.rb
@@ -1183,6 +1200,7 @@ files:
1183
1200
  - lib/openc3/topics/decom_interface_topic.rb
1184
1201
  - lib/openc3/topics/interface_topic.rb
1185
1202
  - lib/openc3/topics/limits_event_topic.rb
1203
+ - lib/openc3/topics/queue_topic.rb
1186
1204
  - lib/openc3/topics/router_topic.rb
1187
1205
  - lib/openc3/topics/system_events_topic.rb
1188
1206
  - lib/openc3/topics/telemetry_decom_topic.rb
@@ -1351,7 +1369,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
1351
1369
  - !ruby/object:Gem::Version
1352
1370
  version: '0'
1353
1371
  requirements: []
1354
- rubygems_version: 3.7.1
1372
+ rubygems_version: 3.7.2
1355
1373
  specification_version: 4
1356
1374
  summary: OpenC3
1357
1375
  test_files: []