openc3 6.4.2 → 6.5.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.
@@ -20,17 +20,17 @@
20
20
  # This file may also be used under the terms of a commercial license
21
21
  # if purchased from OpenC3, Inc.
22
22
 
23
- require 'openc3/version'
24
- require 'openc3/models/model'
25
- require 'openc3/models/plugin_model'
26
- require 'openc3/models/microservice_model'
27
- require 'openc3/models/setting_model'
28
- require 'openc3/models/trigger_group_model'
29
- require 'openc3/topics/system_events_topic'
23
+ require "openc3/version"
24
+ require "openc3/models/model"
25
+ require "openc3/models/plugin_model"
26
+ require "openc3/models/microservice_model"
27
+ require "openc3/models/setting_model"
28
+ require "openc3/models/trigger_group_model"
29
+ require "openc3/topics/system_events_topic"
30
30
 
31
31
  begin
32
- require 'openc3-enterprise/models/cmd_authority_model'
33
- require 'openc3-enterprise/models/critical_cmd_model'
32
+ require "openc3-enterprise/models/cmd_authority_model"
33
+ require "openc3-enterprise/models/critical_cmd_model"
34
34
  module OpenC3
35
35
  class ScopeModel < Model
36
36
  ENTERPRISE = true
@@ -46,7 +46,7 @@ end
46
46
 
47
47
  module OpenC3
48
48
  class ScopeModel < Model
49
- PRIMARY_KEY = 'openc3_scopes'
49
+ PRIMARY_KEY = "openc3_scopes"
50
50
 
51
51
  attr_accessor :children
52
52
  attr_accessor :text_log_cycle_time
@@ -76,17 +76,17 @@ module OpenC3
76
76
  end
77
77
 
78
78
  def self.from_json(json, scope: nil)
79
- json = JSON.parse(json, :allow_nan => true, :create_additions => true) if String === json
79
+ json = JSON.parse(json, allow_nan: true, create_additions: true) if String === json
80
80
  raise "json data is nil" if json.nil?
81
- self.new(**json.transform_keys(&:to_sym))
81
+ new(**json.transform_keys(&:to_sym))
82
82
  end
83
83
 
84
84
  def self.get_model(name:, scope: nil)
85
85
  json = get(name: name)
86
86
  if json
87
- return from_json(json)
87
+ from_json(json)
88
88
  else
89
- return nil
89
+ nil
90
90
  end
91
91
  end
92
92
 
@@ -99,8 +99,7 @@ module OpenC3
99
99
  command_authority: false,
100
100
  critical_commanding: "OFF",
101
101
  shard: 0,
102
- updated_at: nil
103
- )
102
+ updated_at: nil)
104
103
  super(
105
104
  PRIMARY_KEY,
106
105
  name: name,
@@ -117,7 +116,7 @@ module OpenC3
117
116
  @command_authority = command_authority
118
117
  @critical_commanding = critical_commanding.to_s.upcase
119
118
  @critical_commanding = "OFF" if @critical_commanding.length == 0
120
- if not ["OFF", "NORMAL", "ALL"].include?(@critical_commanding)
119
+ if !["OFF", "NORMAL", "ALL"].include?(@critical_commanding)
121
120
  raise "Invalid value for critical_commanding: #{@critical_commanding}"
122
121
  end
123
122
  @shard = shard.to_i # to_i to handle nil
@@ -126,7 +125,7 @@ module OpenC3
126
125
 
127
126
  def create(update: false, force: false, queued: false)
128
127
  # Ensure there are no "." in the scope name - prevents gems accidentally becoming scope names
129
- raise "Invalid scope name: #{@name}" if @name !~ /^[a-zA-Z0-9_-]+$/
128
+ raise "Invalid scope name: #{@name}" if !/^[a-zA-Z0-9_-]+$/.match?(@name)
130
129
  @name = @name.upcase
131
130
  @scope = @name # Ensure @scope matches @name
132
131
  # Ensure the various cycle and retain times are integers
@@ -135,7 +134,7 @@ module OpenC3
135
134
  @text_log_retain_time = @text_log_retain_time.to_i if @text_log_retain_time
136
135
  @tool_log_retain_time = @tool_log_retain_time.to_i if @tool_log_retain_time
137
136
  @cleanup_poll_time = @cleanup_poll_time.to_i
138
- super(update: update, force: force, queued: queued)
137
+ super
139
138
 
140
139
  if ENTERPRISE
141
140
  # If we're updating the scope and disabling command_authority
@@ -157,50 +156,49 @@ module OpenC3
157
156
  end
158
157
  end
159
158
 
160
- SystemEventsTopic.write(:scope, as_json())
159
+ SystemEventsTopic.write(:scope, as_json)
161
160
  end
162
161
 
163
162
  def destroy
164
- if @name != 'DEFAULT'
163
+ if @name != "DEFAULT"
165
164
  # Remove all the plugins for this scope
166
165
  plugins = PluginModel.get_all_models(scope: @name)
167
166
  plugins.each do |_plugin_name, plugin|
168
167
  plugin.destroy
169
168
  end
170
- super()
169
+ super
171
170
  else
172
171
  raise "DEFAULT scope cannot be destroyed"
173
172
  end
174
173
  end
175
174
 
176
175
  def as_json(*_a)
177
- { 'name' => @name,
178
- 'updated_at' => @updated_at,
179
- 'text_log_cycle_time' => @text_log_cycle_time,
180
- 'text_log_cycle_size' => @text_log_cycle_size,
181
- 'text_log_retain_time' => @text_log_retain_time,
182
- 'tool_log_retain_time' => @tool_log_retain_time,
183
- 'cleanup_poll_time' => @cleanup_poll_time,
184
- 'command_authority' => @command_authority,
185
- 'critical_commanding' => @critical_commanding,
186
- 'shard' => @shard,
187
- }
176
+ {"name" => @name,
177
+ "updated_at" => @updated_at,
178
+ "text_log_cycle_time" => @text_log_cycle_time,
179
+ "text_log_cycle_size" => @text_log_cycle_size,
180
+ "text_log_retain_time" => @text_log_retain_time,
181
+ "tool_log_retain_time" => @tool_log_retain_time,
182
+ "cleanup_poll_time" => @cleanup_poll_time,
183
+ "command_authority" => @command_authority,
184
+ "critical_commanding" => @critical_commanding,
185
+ "shard" => @shard}
188
186
  end
189
187
 
190
188
  def deploy_openc3_log_messages_microservice(gem_path, variables, parent)
191
189
  microservice_name = "#{@scope}__OPENC3__LOG"
192
190
  topics = ["#{@scope}__openc3_log_messages"]
193
191
  # Also log the NOSCOPE messages with this microservice for the DEFAULT scope
194
- if @scope == 'DEFAULT'
192
+ if @scope == "DEFAULT"
195
193
  topics << "NOSCOPE__openc3_log_messages"
196
194
  end
197
195
  microservice = MicroserviceModel.new(
198
196
  name: microservice_name,
199
197
  cmd: ["ruby", "text_log_microservice.rb", microservice_name],
200
- work_dir: '/openc3/lib/openc3/microservices',
198
+ work_dir: "/openc3/lib/openc3/microservices",
201
199
  options: [
202
200
  ["CYCLE_TIME", @text_log_cycle_time],
203
- ["CYCLE_SIZE", @text_log_cycle_size],
201
+ ["CYCLE_SIZE", @text_log_cycle_size]
204
202
  ],
205
203
  topics: topics,
206
204
  parent: parent,
@@ -218,11 +216,11 @@ module OpenC3
218
216
  microservice = MicroserviceModel.new(
219
217
  name: microservice_name,
220
218
  cmd: ["ruby", "log_microservice.rb", microservice_name],
221
- work_dir: '/openc3/lib/openc3/microservices',
219
+ work_dir: "/openc3/lib/openc3/microservices",
222
220
  options: [
223
221
  ["RAW_OR_DECOM", "RAW"],
224
222
  ["CMD_OR_TLM", "CMD"],
225
- ["CYCLE_TIME", "3600"], # Keep at most 1 hour per log
223
+ ["CYCLE_TIME", "3600"] # Keep at most 1 hour per log
226
224
  ],
227
225
  topics: ["#{@scope}__COMMAND__{UNKNOWN}__UNKNOWN"],
228
226
  target_names: [],
@@ -241,11 +239,11 @@ module OpenC3
241
239
  microservice = MicroserviceModel.new(
242
240
  name: microservice_name,
243
241
  cmd: ["ruby", "log_microservice.rb", microservice_name],
244
- work_dir: '/openc3/lib/openc3/microservices',
242
+ work_dir: "/openc3/lib/openc3/microservices",
245
243
  options: [
246
244
  ["RAW_OR_DECOM", "RAW"],
247
245
  ["CMD_OR_TLM", "TLM"],
248
- ["CYCLE_TIME", "3600"], # Keep at most 1 hour per log
246
+ ["CYCLE_TIME", "3600"] # Keep at most 1 hour per log
249
247
  ],
250
248
  topics: ["#{@scope}__TELEMETRY__{UNKNOWN}__UNKNOWN"],
251
249
  target_names: [],
@@ -264,7 +262,7 @@ module OpenC3
264
262
  microservice = MicroserviceModel.new(
265
263
  name: microservice_name,
266
264
  cmd: ["ruby", "periodic_microservice.rb", microservice_name],
267
- work_dir: '/openc3/lib/openc3/microservices',
265
+ work_dir: "/openc3/lib/openc3/microservices",
268
266
  parent: parent,
269
267
  shard: @shard,
270
268
  scope: @scope
@@ -280,7 +278,7 @@ module OpenC3
280
278
  microservice = MicroserviceModel.new(
281
279
  name: microservice_name,
282
280
  cmd: ["ruby", "scope_cleanup_microservice.rb", microservice_name],
283
- work_dir: '/openc3/lib/openc3/microservices',
281
+ work_dir: "/openc3/lib/openc3/microservices",
284
282
  parent: parent,
285
283
  shard: @shard,
286
284
  scope: @scope
@@ -296,7 +294,7 @@ module OpenC3
296
294
  microservice = MicroserviceModel.new(
297
295
  name: microservice_name,
298
296
  cmd: ["ruby", "critical_cmd_microservice.rb", microservice_name],
299
- work_dir: '/openc3-enterprise/lib/openc3-enterprise/microservices',
297
+ work_dir: "/openc3-enterprise/lib/openc3-enterprise/microservices",
300
298
  parent: parent,
301
299
  shard: @shard,
302
300
  scope: @scope
@@ -312,7 +310,7 @@ module OpenC3
312
310
  microservice = MicroserviceModel.new(
313
311
  name: microservice_name,
314
312
  cmd: ["ruby", "multi_microservice.rb", *@children],
315
- work_dir: '/openc3/lib/openc3/microservices',
313
+ work_dir: "/openc3/lib/openc3/microservices",
316
314
  target_names: [],
317
315
  shard: @shard,
318
316
  scope: @scope
@@ -323,15 +321,15 @@ module OpenC3
323
321
  end
324
322
 
325
323
  def deploy(gem_path, variables)
326
- seed_database()
324
+ seed_database
327
325
 
328
326
  if ENTERPRISE
329
327
  # Create DEFAULT trigger group model
330
- model = TriggerGroupModel.get(name: 'DEFAULT', scope: @scope)
328
+ model = TriggerGroupModel.get(name: "DEFAULT", scope: @scope)
331
329
  unless model
332
- model = TriggerGroupModel.new(name: 'DEFAULT', shard: @shard, scope: @scope)
333
- model.create()
334
- model.deploy()
330
+ model = TriggerGroupModel.new(name: "DEFAULT", shard: @shard, scope: @scope)
331
+ model.create
332
+ model.deploy
335
333
  end
336
334
  end
337
335
 
@@ -351,7 +349,7 @@ module OpenC3
351
349
  deploy_unknown_packetlog_microservice(gem_path, variables, @parent)
352
350
 
353
351
  # Only DEFAULT scope
354
- if @scope == 'DEFAULT'
352
+ if @scope == "DEFAULT"
355
353
  # Periodic Microservice
356
354
  deploy_periodic_microservice(gem_path, variables, @parent)
357
355
  end
@@ -403,14 +401,46 @@ module OpenC3
403
401
  end
404
402
 
405
403
  def seed_database
406
- setting = SettingModel.get(name: 'source_url')
407
- SettingModel.set({ name: 'source_url', data: 'https://github.com/OpenC3/cosmos' }, scope: @scope) unless setting
408
- setting = SettingModel.get(name: 'rubygems_url')
409
- SettingModel.set({ name: 'rubygems_url', data: ENV['RUBYGEMS_URL'] || 'https://rubygems.org' }, scope: @scope) unless setting
410
- setting = SettingModel.get(name: 'pypi_url')
411
- SettingModel.set({ name: 'pypi_url', data: ENV['PYPI_URL'] || 'https://pypi.org' }, scope: @scope) unless setting
404
+ setting = SettingModel.get(name: "source_url")
405
+ SettingModel.set({name: "source_url", data: "https://github.com/OpenC3/cosmos"}, scope: @scope) unless setting
406
+ setting = SettingModel.get(name: "rubygems_url")
407
+ SettingModel.set({name: "rubygems_url", data: ENV["RUBYGEMS_URL"] || "https://rubygems.org"}, scope: @scope) unless setting
408
+ setting = SettingModel.get(name: "pypi_url")
409
+ SettingModel.set({name: "pypi_url", data: ENV["PYPI_URL"] || "https://pypi.org"}, scope: @scope) unless setting
412
410
  # Set the news feed to true by default, don't bother checking if it's already set
413
- SettingModel.set({ name: 'news_feed', data: true }, scope: @scope)
411
+ SettingModel.set({name: "news_feed", data: true}, scope: @scope)
412
+
413
+ setting = SettingModel.get(name: "system_health")
414
+ system_health_data = {
415
+ "cpu" => {
416
+ "redThreshold" => 90.0,
417
+ "yellowThreshold" => 80.0,
418
+ "snoozeMinutes" => 15,
419
+ "lastTriggerTimeRed" => nil, # timestamp or nil
420
+ "lastTriggerTimeYellow" => nil, # timestamp or nil
421
+ "sustainedSeconds" => 15
422
+ },
423
+ "memory" => {
424
+ "redThreshold" => 90.0,
425
+ "yellowThreshold" => 80.0,
426
+ "snoozeMinutes" => 15,
427
+ "lastTriggerTimeRed" => nil, # timestamp or nil
428
+ "lastTriggerTimeYellow" => nil, # timestamp or nil
429
+ "sustainedSeconds" => 15
430
+ },
431
+ "disk" => {
432
+ "redThreshold" => 90.0,
433
+ "yellowThreshold" => 80.0,
434
+ "snoozeMinutes" => 720, # 12 hours
435
+ "lastTriggerTimeRed" => nil, # timestamp or nil
436
+ "lastTriggerTimeYellow" => nil, # timestamp or nil
437
+ "sustainedSeconds" => 60
438
+ },
439
+ "global" => {
440
+ "enableAlerts" => true
441
+ }
442
+ }
443
+ SettingModel.set({name: "system_health", data: system_health_data}, scope: "DEFAULT") unless setting
414
444
  end
415
445
  end
416
446
  end
@@ -0,0 +1,93 @@
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/top_level'
20
+ require 'openc3/models/model'
21
+ require 'openc3/models/scope_model'
22
+ require 'openc3/utilities/bucket'
23
+ require 'openc3/utilities/bucket_utilities'
24
+
25
+ module OpenC3
26
+ class ScriptEngineModel < Model
27
+ PRIMARY_KEY = 'openc3_script_engines'
28
+
29
+ attr_accessor :filename # Script Engine filename
30
+
31
+ # NOTE: The following three class methods are used by the ModelController
32
+ # and are reimplemented to enable various Model class methods to work
33
+ def self.get(name:, scope: nil)
34
+ super(PRIMARY_KEY, name: name)
35
+ end
36
+
37
+ def self.names(scope: nil)
38
+ array = []
39
+ all(scope: scope).each do |name, _script_engine|
40
+ array << name
41
+ end
42
+ array
43
+ end
44
+
45
+ def self.all(scope: nil)
46
+ tools = Store.hgetall(PRIMARY_KEY)
47
+ tools.each do |key, value|
48
+ tools[key] = JSON.parse(value, :allow_nan => true, :create_additions => true)
49
+ end
50
+ return tools
51
+ end
52
+
53
+ # Called by the PluginModel to allow this class to validate it's top-level keyword: "SCRIPT_ENGINE"
54
+ def self.handle_config(parser, keyword, parameters, plugin: nil, needs_dependencies: false, scope:)
55
+ case keyword
56
+ when 'SCRIPT_ENGINE'
57
+ parser.verify_num_parameters(1, 3, "SCRIPT_ENGINE <Extension> <Filename>")
58
+ return self.new(name: parameters[0], plugin: plugin, filename: parameters[1], scope: scope)
59
+ else
60
+ raise ConfigParser::Error.new(parser, "Unknown keyword and parameters for Script Engine: #{keyword} #{parameters.join(" ")}")
61
+ end
62
+ return nil
63
+ end
64
+
65
+ def initialize(
66
+ name:,
67
+ updated_at: nil,
68
+ plugin: nil,
69
+ filename: nil,
70
+ scope:
71
+ )
72
+ super(PRIMARY_KEY, name: name, plugin: plugin, updated_at: updated_at, scope: scope)
73
+ @filename = filename
74
+ end
75
+
76
+ def as_json(*a)
77
+ {
78
+ 'name' => @name,
79
+ 'updated_at' => @updated_at,
80
+ 'plugin' => @plugin,
81
+ 'filename' => @filename
82
+ }
83
+ end
84
+
85
+ def handle_config(parser, keyword, parameters)
86
+ raise ConfigParser::Error.new(parser, "Unknown keyword and parameters for Script Engine: #{keyword} #{parameters.join(" ")}")
87
+ end
88
+
89
+ def deploy(gem_path, variables, validate_only: false)
90
+ # Nothing to do
91
+ end
92
+ end
93
+ end
@@ -45,6 +45,7 @@ module OpenC3
45
45
  attr_accessor :pid
46
46
  attr_accessor :log
47
47
  attr_accessor :report
48
+ attr_accessor :script_engine
48
49
 
49
50
  # NOTE: The following three class methods are used by the ModelController
50
51
  # and are reimplemented to enable various Model class methods to work
@@ -147,6 +148,7 @@ module OpenC3
147
148
  pid: nil,
148
149
  log: nil,
149
150
  report: nil,
151
+ script_engine: nil,
150
152
  updated_at: nil,
151
153
  scope:
152
154
  )
@@ -173,6 +175,7 @@ module OpenC3
173
175
  @pid = pid
174
176
  @log = log
175
177
  @report = report
178
+ @script_engine = script_engine
176
179
  end
177
180
 
178
181
  def is_complete?
@@ -257,6 +260,7 @@ module OpenC3
257
260
  'pid' => @pid,
258
261
  'log' => @log,
259
262
  'report' => @report,
263
+ 'script_engine' => @script_engine,
260
264
  'updated_at' => @updated_at,
261
265
  'scope' => @scope
262
266
  }
@@ -676,6 +676,8 @@ module OpenC3
676
676
  LimitsEventTopic.delete(@name, scope: @scope)
677
677
  Store.del("#{@scope}__openc3tlm__#{@name}")
678
678
  Store.del("#{@scope}__openc3cmd__#{@name}")
679
+ Store.del("#{@scope}__TELEMETRYCNTS__{#{@name}}")
680
+ Store.del("#{@scope}__COMMANDCNTS__{#{@name}}")
679
681
 
680
682
  # Note: these match the names of the services in deploy_microservices
681
683
  %w(MULTI DECOM COMMANDLOG DECOMCMDLOG PACKETLOG DECOMLOG REDUCER CLEANUP).each do |type|
@@ -795,6 +797,7 @@ module OpenC3
795
797
  if clear_old
796
798
  Store.del("#{@scope}__openc3tlm__#{target_name}")
797
799
  Store.del("#{@scope}__openc3tlm__#{target_name}__allitems")
800
+ Store.del("#{@scope}__TELEMETRYCNTS__{#{target_name}}")
798
801
  end
799
802
  packets.each do |packet_name, packet|
800
803
  Logger.debug "Configuring tlm packet: #{target_name} #{packet_name}"
@@ -816,7 +819,10 @@ module OpenC3
816
819
 
817
820
  def update_store_commands(packet_hash, clear_old: true)
818
821
  packet_hash.each do |target_name, packets|
819
- Store.del("#{@scope}__openc3cmd__#{target_name}") if clear_old
822
+ if clear_old
823
+ Store.del("#{@scope}__openc3cmd__#{target_name}")
824
+ Store.del("#{@scope}__COMMANDCNTS__{#{target_name}}")
825
+ end
820
826
  packets.each do |packet_name, packet|
821
827
  Logger.debug "Configuring cmd packet: #{target_name} #{packet_name}"
822
828
  begin
@@ -226,7 +226,11 @@ module OpenC3
226
226
  # script_run("INST/procedures/collect.rb")
227
227
  #
228
228
  def initialize_offline_access
229
- auth = OpenC3KeycloakAuthentication.new(ENV['OPENC3_KEYCLOAK_URL'])
229
+ keycloak_url = ENV['OPENC3_KEYCLOAK_URL']
230
+ if keycloak_url.nil? or keycloak_url.empty?
231
+ raise "initialize_offline_access only valid in COSMOS Enterprise. OPENC3_KEYCLOAK_URL environment variable must be set."
232
+ end
233
+ auth = OpenC3KeycloakAuthentication.new(keycloak_url)
230
234
  auth.token(include_bearer: true, openid_scope: 'openid%20offline_access')
231
235
  set_offline_access(auth.refresh_token)
232
236
  end
@@ -0,0 +1,118 @@
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
+ module OpenC3
20
+ class ScriptEngine
21
+ attr_accessor :running_script
22
+
23
+ def initialize(running_script)
24
+ @running_script = running_script
25
+ end
26
+
27
+ # Override this method in the subclass to implement the script engine
28
+ def run_line(line, lines, filename, line_no)
29
+ puts line
30
+ return line_no + 1
31
+ end
32
+
33
+ def run_text(text, filename: nil, line_no: 1, end_line_no: nil, bind_variables: false)
34
+ lines = text.lines
35
+ loop do
36
+ line = lines[line_no - 1]
37
+ return if line.nil?
38
+
39
+ begin
40
+ next_line_no = line_no + 1
41
+ running_script.pre_line_instrumentation(filename, line_no)
42
+ next_line_no = run_line(line, lines, filename, line_no)
43
+ running_script.post_line_instrumentation(filename, line_no)
44
+ rescue Exception => e
45
+ retry if running_script.exception_instrumentation(e, filename, line_no)
46
+ end
47
+
48
+ line_no = next_line_no
49
+ return if end_line_no and line_no > end_line_no
50
+ end
51
+ end
52
+
53
+ def debug(text)
54
+ run_line(text, [text], "DEBUG", 1)
55
+ end
56
+
57
+ def syntax_check(text, filename: nil)
58
+ puts "Not Implemented"
59
+ return 1
60
+ end
61
+
62
+ def mnemonic_check(text, filename: nil)
63
+ puts "Not Implemented"
64
+ return 1
65
+ end
66
+
67
+ def tokenizer(s, special_chars = '()><+-*/=;,')
68
+ result = []
69
+ i = 0
70
+ while i < s.length
71
+ # Skip whitespace
72
+ if s[i].match?(/\s/)
73
+ i += 1
74
+ next
75
+ end
76
+
77
+ # Handle quoted strings (single or double quotes)
78
+ if ['"', "'"].include?(s[i])
79
+ quote_char = s[i]
80
+ quote_start = i
81
+ i += 1
82
+ # Find the closing quote
83
+ while i < s.length
84
+ if s[i] == '\\' && i + 1 < s.length # Handle escaped characters
85
+ i += 2
86
+ elsif s[i] == quote_char # Found closing quote
87
+ i += 1
88
+ break
89
+ else
90
+ i += 1
91
+ end
92
+ end
93
+ # Include the quotes in the token
94
+ result << s[quote_start...i]
95
+ next
96
+ end
97
+
98
+ # Handle special characters
99
+ if special_chars.include?(s[i])
100
+ result << s[i]
101
+ i += 1
102
+ next
103
+ end
104
+
105
+ # Handle regular tokens
106
+ token_start = i
107
+ while i < s.length && !s[i].match?(/\s/) && !special_chars.include?(s[i]) && !['"', "'"].include?(s[i])
108
+ i += 1
109
+ end
110
+ if i > token_start
111
+ result << s[token_start...i]
112
+ end
113
+ end
114
+
115
+ return result
116
+ end
117
+ end
118
+ end
@@ -24,6 +24,8 @@ require 'openc3/topics/topic'
24
24
 
25
25
  module OpenC3
26
26
  class InterfaceTopic < Topic
27
+ COMMAND_ACK_TIMEOUT_S = 30
28
+
27
29
  # Generate a list of topics for this interface. This includes the interface itself
28
30
  # and all the targets which are assigned to this interface.
29
31
  def self.topics(interface, scope:)
@@ -48,9 +50,27 @@ module OpenC3
48
50
  end
49
51
  end
50
52
 
51
- def self.write_raw(interface_name, data, scope:)
52
- Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'raw' => data }, '*', 100)
53
- # Todo: This should wait for the ack
53
+ def self.write_raw(interface_name, data, timeout: nil, scope:)
54
+ interface_name = interface_name.upcase
55
+
56
+ timeout = COMMAND_ACK_TIMEOUT_S unless timeout
57
+ ack_topic = "{#{scope}__ACKCMD}INTERFACE__#{interface_name}"
58
+ Topic.update_topic_offsets([ack_topic])
59
+
60
+ cmd_id = Topic.write_topic("{#{scope}__CMD}INTERFACE__#{interface_name}", { 'raw' => data }, '*', 100)
61
+ time = Time.now
62
+ while (Time.now - time) < timeout
63
+ Topic.read_topics([ack_topic]) do |_topic, _msg_id, msg_hash, _redis|
64
+ if msg_hash["id"] == cmd_id
65
+ if msg_hash["result"] == "SUCCESS"
66
+ return
67
+ else
68
+ raise msg_hash["result"]
69
+ end
70
+ end
71
+ end
72
+ end
73
+ raise "Timeout of #{timeout}s waiting for cmd ack"
54
74
  end
55
75
 
56
76
  def self.connect_interface(interface_name, *interface_params, scope:)