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
@@ -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
@@ -1088,6 +1088,25 @@ module OpenC3
1088
1088
  Logger.info "Configured microservice #{microservice_name}"
1089
1089
  end
1090
1090
 
1091
+ def deploy_tsdb_microservice(gem_path, variables, topics, instance = nil, parent = nil)
1092
+ microservice_name = "#{@scope}__TSDB#{instance}__#{@name}"
1093
+ microservice = MicroserviceModel.new(
1094
+ name: microservice_name,
1095
+ folder_name: @folder_name,
1096
+ cmd: ["python", "quest_microservice.py", microservice_name],
1097
+ work_dir: "/openc3/python/openc3/microservices",
1098
+ topics: topics,
1099
+ plugin: @plugin,
1100
+ parent: nil,
1101
+ needs_dependencies: @needs_dependencies,
1102
+ shard: @shard,
1103
+ scope: @scope
1104
+ )
1105
+ microservice.create
1106
+ microservice.deploy(gem_path, variables)
1107
+ Logger.info "Configured microservice #{microservice_name}"
1108
+ end
1109
+
1091
1110
  def deploy_reducer_microservice(gem_path, variables, topics, instance = nil, parent = nil)
1092
1111
  microservice_name = "#{@scope}__REDUCER#{instance}__#{@name}"
1093
1112
  microservice = MicroserviceModel.new(
@@ -1250,6 +1269,13 @@ module OpenC3
1250
1269
  deploy_decom_microservice(system.targets[@name], gem_path, variables, topics, instance, parent)
1251
1270
  end
1252
1271
 
1272
+ # TSDB Microservice
1273
+ if ENV['OPENC3_TSDB_HOSTNAME'] and ENV['OPENC3_TSDB_QUERY_PORT'] and ENV['OPENC3_TSDB_INGEST_PORT'] and ENV['OPENC3_TSDB_USERNAME'] and ENV['OPENC3_TSDB_PASSWORD']
1274
+ deploy_target_microservices('TSDB', decom_topic_list, "#{@scope}__DECOM__{#{@name}}") do |topics, instance, parent|
1275
+ deploy_tsdb_microservice(gem_path, variables, topics, instance, parent)
1276
+ end
1277
+ end
1278
+
1253
1279
  # Reducer Microservice
1254
1280
  unless @reducer_disable
1255
1281
  # TODO: Does Reducer even need a topic list?
@@ -184,7 +184,7 @@ module OpenC3
184
184
  end
185
185
  end
186
186
 
187
- if @url and !@url.start_with?('/') and !@url.start_with?('http')
187
+ if @url and !@url.start_with?('/') and @url !~ URI::regexp
188
188
  raise "URL must be a full URL (http://domain.com/path) or a relative path (/path)"
189
189
  end
190
190
 
@@ -334,7 +334,7 @@ module OpenC3
334
334
  synchronize() do
335
335
  begin
336
336
  internal_buffer_equals(buffer)
337
- rescue RuntimeError => e
337
+ rescue RuntimeError
338
338
  Logger.instance.error "#{@target_name} #{@packet_name} received with actual packet length of #{buffer.length} but defined length of #{@defined_length}"
339
339
  end
340
340
  @read_conversion_cache.clear if @read_conversion_cache
@@ -1315,7 +1315,7 @@ module OpenC3
1315
1315
 
1316
1316
  begin
1317
1317
  current_value = read(item.name, :RAW)
1318
-
1318
+
1319
1319
  case current_value
1320
1320
  when Array
1321
1321
  # For arrays, create a new array of zeros with the same size
@@ -1325,7 +1325,7 @@ module OpenC3
1325
1325
  when :FLOAT
1326
1326
  obfuscated_value = Array.new(current_value.size, 0.0)
1327
1327
  when :STRING, :BLOCK
1328
- obfuscated_value = Array.new(current_value.size) { |i|
1328
+ obfuscated_value = Array.new(current_value.size) { |i|
1329
1329
  "\x00" * current_value[i].length if current_value[i]
1330
1330
  }
1331
1331
  else
@@ -80,7 +80,13 @@ module OpenC3
80
80
  if data_type == :STRING || data_type == :BLOCK
81
81
  @parser.parameters[1]
82
82
  else
83
- @parser.parameters[1].convert_to_value
83
+ value = @parser.parameters[1].convert_to_value
84
+ # Check if the value is a string, which indicates an error in parsing
85
+ # except for 'ANY' which is a valid state value
86
+ if value.is_a?(String) and value != "ANY"
87
+ raise @parser.error("Invalid state value #{value} for data type #{data_type}.", @usage)
88
+ end
89
+ return value
84
90
  end
85
91
  end
86
92
 
@@ -603,7 +603,7 @@ module OpenC3
603
603
  if item.variable_bit_size
604
604
  # Bit size is determined by length field
605
605
  length_value = self.read(item.variable_bit_size['length_item_name'], :CONVERTED)
606
- if item.data_type == :INT or item.data_type == :UINT and not item.original_array_size
606
+ if (item.data_type == :INT or item.data_type == :UINT) and not item.original_array_size
607
607
  case length_value
608
608
  when 0
609
609
  return 6
@@ -641,7 +641,14 @@ module OpenC3
641
641
  # Calculate the actual current size of this variable length item
642
642
  new_bit_size = calculate_total_bit_size(item)
643
643
 
644
- if item.original_bit_size != new_bit_size
644
+ if item.original_array_size
645
+ # Array size has changed from original - so we need to adjust everything after this item
646
+ # This includes items that may have the same bit_offset as the variable length item because it
647
+ # started out at zero bit_size
648
+ if item.original_array_size != new_bit_size
649
+ adjustment += (new_bit_size - item.original_array_size)
650
+ end
651
+ elsif item.original_bit_size != new_bit_size
645
652
  # Bit size has changed from original - so we need to adjust everything after this item
646
653
  # This includes items that may have the same bit_offset as the variable length item because it
647
654
  # started out at zero bit_size
@@ -25,7 +25,7 @@ module OpenC3
25
25
 
26
26
  def list_timelines(scope: $openc3_scope)
27
27
  response = $api_server.request('get', "/openc3-api/timeline", scope: scope)
28
- return _handle_response(response, 'Failed to list timelines')
28
+ return _cal_handle_response(response, 'Failed to list timelines')
29
29
  end
30
30
 
31
31
  def create_timeline(name, color: nil, scope: $openc3_scope)
@@ -33,19 +33,19 @@ module OpenC3
33
33
  data['name'] = name
34
34
  data['color'] = color if color
35
35
  response = $api_server.request('post', "/openc3-api/timeline", data: data, json: true, scope: scope)
36
- return _handle_response(response, 'Failed to create timeline')
36
+ return _cal_handle_response(response, 'Failed to create timeline')
37
37
  end
38
38
 
39
39
  def get_timeline(name, scope: $openc3_scope)
40
40
  response = $api_server.request('get', "/openc3-api/timeline/#{name}", scope: scope)
41
- return _handle_response(response, 'Failed to get timeline')
41
+ return _cal_handle_response(response, 'Failed to get timeline')
42
42
  end
43
43
 
44
44
  def set_timeline_color(name, color, scope: $openc3_scope)
45
45
  post_data = {}
46
46
  post_data['color'] = color
47
47
  response = $api_server.request('post', "/openc3-api/timeline/#{name}/color", data: post_data, json: true, scope: scope)
48
- return _handle_response(response, 'Failed to set timeline color')
48
+ return _cal_handle_response(response, 'Failed to set timeline color')
49
49
  end
50
50
 
51
51
  def delete_timeline(name, force: false, scope: $openc3_scope)
@@ -54,7 +54,7 @@ module OpenC3
54
54
  url += "?force=true"
55
55
  end
56
56
  response = $api_server.request('delete', url, scope: scope)
57
- return _handle_response(response, 'Failed to delete timeline')
57
+ return _cal_handle_response(response, 'Failed to delete timeline')
58
58
  end
59
59
 
60
60
  def create_timeline_activity(name, kind:, start:, stop:, data: {}, scope: $openc3_scope)
@@ -69,12 +69,12 @@ module OpenC3
69
69
  post_data['kind'] = kind
70
70
  post_data['data'] = data
71
71
  response = $api_server.request('post', "/openc3-api/timeline/#{name}/activities", data: post_data, json: true, scope: scope)
72
- return _handle_response(response, 'Failed to create timeline activity')
72
+ return _cal_handle_response(response, 'Failed to create timeline activity')
73
73
  end
74
74
 
75
75
  def get_timeline_activity(name, start, uuid, scope: $openc3_scope)
76
76
  response = $api_server.request('get', "/openc3-api/timeline/#{name}/activity/#{start}/#{uuid}", scope: scope)
77
- return _handle_response(response, 'Failed to get timeline activity')
77
+ return _cal_handle_response(response, 'Failed to get timeline activity')
78
78
  end
79
79
 
80
80
  def get_timeline_activities(name, start: nil, stop: nil, limit: nil, scope: $openc3_scope)
@@ -86,16 +86,16 @@ module OpenC3
86
86
  url += "?limit=#{limit}"
87
87
  end
88
88
  response = $api_server.request('get', url, scope: scope)
89
- return _handle_response(response, 'Failed to get timeline activities')
89
+ return _cal_handle_response(response, 'Failed to get timeline activities')
90
90
  end
91
91
 
92
92
  def delete_timeline_activity(name, start, uuid, scope: $openc3_scope)
93
93
  response = $api_server.request('delete', "/openc3-api/timeline/#{name}/activity/#{start}/#{uuid}", scope: scope)
94
- return _handle_response(response, 'Failed to delete timeline activity')
94
+ return _cal_handle_response(response, 'Failed to delete timeline activity')
95
95
  end
96
96
 
97
97
  # Helper method to handle the response
98
- def _handle_response(response, error_message)
98
+ def _cal_handle_response(response, error_message)
99
99
  return nil if response.nil?
100
100
  if response.status >= 400
101
101
  result = JSON.parse(response.body, :allow_nan => true, :create_additions => true)
@@ -115,14 +115,14 @@ module OpenC3
115
115
  end
116
116
  end
117
117
  _log_cmd(command, raw, no_range, no_hazardous)
118
- end
118
+ end
119
119
 
120
120
  # Send the command and log the results
121
121
  # This method signature has to include the keyword params present in cmd_api.rb _cmd_implementation()
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'
@@ -71,7 +71,7 @@ module OpenC3
71
71
  end
72
72
  end
73
73
 
74
- def script_run(filename, disconnect: false, environment: nil, scope: $openc3_scope)
74
+ def script_run(filename, disconnect: false, environment: nil, suite_runner: nil, scope: $openc3_scope)
75
75
  if disconnect
76
76
  endpoint = "/script-api/scripts/#{filename}/run/disconnect"
77
77
  else
@@ -87,8 +87,13 @@ module OpenC3
87
87
  else
88
88
  env_data = []
89
89
  end
90
+ data = { environment: env_data }
91
+ if suite_runner
92
+ # TODO 7.0: Should suiteRunner be snake case?
93
+ data['suiteRunner'] = suite_runner
94
+ end
90
95
  # NOTE: json: true causes json_api_object to JSON generate and set the Content-Type to json
91
- response = $script_runner_api_server.request('post', endpoint, json: true, data: { environment: env_data }, scope: scope)
96
+ response = $script_runner_api_server.request('post', endpoint, json: true, data: data, scope: scope)
92
97
  if response.nil? || response.status != 200
93
98
  _script_response_error(response, "Failed to run #{filename}", scope: scope)
94
99
  else
@@ -24,7 +24,7 @@ module OpenC3
24
24
  post_data = {}
25
25
  post_data['definition'] = definition
26
26
  response = $api_server.request('post', '/openc3-api/tables/generate', json: true, data: post_data, scope: scope)
27
- return _handle_response(response, 'Failed to create binary')
27
+ return _tables_handle_response(response, 'Failed to create binary')
28
28
  end
29
29
 
30
30
  def table_create_report(filename, definition, table_name: nil, scope: $openc3_scope)
@@ -33,11 +33,11 @@ module OpenC3
33
33
  post_data['definition'] = definition
34
34
  post_data['table_name'] = table_name if table_name
35
35
  response = $api_server.request('post', '/openc3-api/tables/report', json: true, data: post_data, scope: scope)
36
- return _handle_response(response, 'Failed to create report')
36
+ return _tables_handle_response(response, 'Failed to create report')
37
37
  end
38
38
 
39
39
  # Helper method to handle the response
40
- def _handle_response(response, error_message)
40
+ def _tables_handle_response(response, error_message)
41
41
  return nil if response.nil?
42
42
  if response.status >= 400
43
43
  result = JSON.parse(response.body, :allow_nan => true, :create_additions => true)
@@ -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
@@ -50,7 +50,7 @@ rescue LoadError
50
50
  end
51
51
 
52
52
  def user_info(_token)
53
- {} # EE does stuff here
53
+ {} # Enterprise does stuff here
54
54
  end
55
55
  end
56
56
  end
@@ -29,7 +29,7 @@ module OpenC3
29
29
  user = {}
30
30
  end
31
31
  username = user['username']
32
- # Core username (EE has the actual username)
32
+ # Core username (Enterprise has the actual username)
33
33
  username ||= 'anonymous'
34
34
  end
35
35
  end