openc3 6.10.2 → 6.10.4

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/bin/openc3cli +15 -4
  3. data/data/config/item_modifiers.yaml +2 -2
  4. data/data/config/parameter_modifiers.yaml +2 -2
  5. data/data/config/plugins.yaml +38 -0
  6. data/data/config/screen.yaml +23 -0
  7. data/data/config/target.yaml +8 -3
  8. data/data/config/widgets.yaml +30 -0
  9. data/ext/openc3/ext/config_parser/config_parser.c +49 -37
  10. data/lib/openc3/accessors/accessor.rb +2 -2
  11. data/lib/openc3/accessors/json_accessor.rb +1 -1
  12. data/lib/openc3/api/tlm_api.rb +1 -6
  13. data/lib/openc3/config/config_parser.rb +45 -25
  14. data/lib/openc3/io/json_api_object.rb +38 -14
  15. data/lib/openc3/io/json_drb_object.rb +29 -11
  16. data/lib/openc3/io/json_rpc.rb +20 -9
  17. data/lib/openc3/microservices/interface_microservice.rb +8 -3
  18. data/lib/openc3/models/plugin_model.rb +40 -9
  19. data/lib/openc3/models/target_model.rb +2 -1
  20. data/lib/openc3/packets/packet.rb +5 -2
  21. data/lib/openc3/packets/packet_config.rb +4 -2
  22. data/lib/openc3/packets/parsers/packet_item_parser.rb +9 -0
  23. data/lib/openc3/packets/parsers/xtce_converter.rb +464 -100
  24. data/lib/openc3/script/web_socket_api.rb +1 -1
  25. data/lib/openc3/topics/command_decom_topic.rb +3 -2
  26. data/lib/openc3/utilities/cli_generator.rb +1 -1
  27. data/lib/openc3/version.rb +5 -5
  28. data/templates/plugin/plugin.gemspec +1 -1
  29. data/templates/tool_angular/package.json +2 -2
  30. data/templates/tool_react/package.json +1 -1
  31. data/templates/tool_svelte/package.json +1 -1
  32. data/templates/tool_vue/package.json +3 -3
  33. data/templates/widget/package.json +2 -2
  34. metadata +15 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5cc7320f928c8941c9d4ecc2a4bc04eade3ebde86c278dfdad90aa7ac24f9d2
4
- data.tar.gz: eb9cd8b34986f982c187dbccbdf0c4067f61df733161cab067a5c689b1ba53a9
3
+ metadata.gz: e6cc1e03ca8cd4ac75025fc7cd4758099c513957b012291e7b332e8c61c513cf
4
+ data.tar.gz: '058a71978241f50cc97a1ebefe70a7a6ecc69a9f1fb51e11690d50fd167ae60f'
5
5
  SHA512:
6
- metadata.gz: 5973122351e2b191d365092984db9c23ad877c8fc71c7173b014fa3a5ae363258a2eeca7863571d9206be0f781ff361586843eac57d7fa56065fef515398c06b
7
- data.tar.gz: 7b664d2a9a8615a5c651002b412cd7248ca0b33e67f6af0618b04354f3ea080b5d81f7d8031ea4e98a013ca2ca4714e3fdb6b30ae5302223d7bdafd88d8c63bb
6
+ metadata.gz: a96529177ee05a9f04257e4668f245c7346692b039e000f970166fb311c80992401f19def70cbe63844510aa97b2a90ab89bafd835b377aecd14ef0505cb2feb
7
+ data.tar.gz: 3a1c5152b36c733009daaefb07786087b428bbf172dd3ecbee2cfb4bb82de88c84ac0af727c25287cd4af5d1038812e1f4a3d2566c935cf0c519b984163013f5
data/bin/openc3cli CHANGED
@@ -33,6 +33,7 @@ require 'openc3/models/queue_model'
33
33
  require 'openc3/models/scope_model'
34
34
  require 'openc3/models/tool_model'
35
35
  require 'openc3/packets/packet_config'
36
+ require 'openc3/packets/parsers/xtce_converter'
36
37
  require 'openc3/utilities/bucket'
37
38
  require 'openc3/utilities/cli_generator'
38
39
  require 'openc3/utilities/local_mode'
@@ -107,7 +108,8 @@ def xtce_converter(args)
107
108
  options = {}
108
109
  option_parser = OptionParser.new do |opts|
109
110
  opts.banner = "Usage: xtce_converter [options] --import input_xtce_filename --output output_dir\n"+
110
- " xtce_converter [options] --plugin /PATH/FILENAME.gem --output output_dir --variables variables.txt"
111
+ " xtce_converter [options] --plugin /PATH/FILENAME.gem --output output_dir "+
112
+ " --variables variables.txt --root_target root_target_name --time_association_name time_association_name"
111
113
  opts.separator("")
112
114
  opts.on("-h", "--help", "Show this message") do
113
115
  puts opts
@@ -125,6 +127,12 @@ def xtce_converter(args)
125
127
  opts.on("-v", "--variables", "Optional variables file to pass to the plugin") do |arg|
126
128
  options[:variables] = arg
127
129
  end
130
+ opts.on("-r ROOT_TARGET", "--root_target ROOT_TARGET", "Optional flag to set which target is at the root of an XTCE document. If not specified, each target will be placed under a generic 'root' spacesystem") do |arg|
131
+ options[:root_target_name] = arg
132
+ end
133
+ opts.on("-t TIME_ASSOCIATION_NAME", "--time_association_name TIME_ASSOCIATION_NAME", "Optional flag to set which target is at the root of an XTCE document. If not specified, each target will be placed under a generic 'root' spacesystem") do |arg|
134
+ options[:time_association_name] = "PACKET_TIME"
135
+ end
128
136
  end
129
137
 
130
138
  begin
@@ -156,8 +164,10 @@ def xtce_converter(args)
156
164
  puts "Installing #{File.basename(options[:plugin])}"
157
165
  plugin_hash = OpenC3::PluginModel.install_phase1(options[:plugin], existing_variables: variables, scope: 'DEFAULT', validate_only: true)
158
166
  plugin_hash['variables']['xtce_output'] = options[:output]
167
+ plugin_hash['variables']['time_association_name'] = options[:time_association_name]
159
168
  OpenC3::PluginModel.install_phase2(plugin_hash, scope: 'DEFAULT', validate_only: true,
160
169
  gem_file_path: options[:plugin])
170
+ OpenC3::XtceConverter.combine_output_xtce(options[:output], options[:root_target_name])
161
171
  result = 0 # bash and Windows consider 0 success
162
172
  rescue => e
163
173
  puts "Error: #{e.message}"
@@ -371,9 +381,10 @@ def load_plugin(plugin_file_path, scope:, plugin_hash_file: nil, force: false, v
371
381
  gem_name = full_name.split('-')[0..-2].join('-')
372
382
  if file_gem_name == gem_name
373
383
  found = true
374
- # Upgrade if version changed else do nothing
375
- if file_full_name != full_name
376
- update_plugin(plugin_file_path, plugin_name, scope: scope, existing_plugin_name: plugin_name, force: force)
384
+ force_install = force || ENV['OPENC3_FORCE_INSTALL']
385
+ # Upgrade if version changed or force install is set
386
+ if file_full_name != full_name || force_install
387
+ update_plugin(plugin_file_path, plugin_name, scope: scope, existing_plugin_name: plugin_name, force: force_install)
377
388
  else
378
389
  puts "No version change detected for: #{plugin_name}"
379
390
  end
@@ -79,12 +79,12 @@ GENERIC_READ_CONVERSION_START:
79
79
  ruby_example: |
80
80
  APPEND_ITEM ITEM1 32 UINT
81
81
  GENERIC_READ_CONVERSION_START
82
- return (value * 1.5).to_i # Convert the value by a scale factor
82
+ (value * 1.5).to_i # Convert the value by a scale factor
83
83
  GENERIC_READ_CONVERSION_END
84
84
  python_example: |
85
85
  APPEND_ITEM ITEM1 32 UINT
86
86
  GENERIC_READ_CONVERSION_START
87
- return int(value * 1.5) # Convert the value by a scale factor
87
+ int(value * 1.5) # Convert the value by a scale factor
88
88
  GENERIC_READ_CONVERSION_END
89
89
  parameters:
90
90
  - name: Converted Type
@@ -141,12 +141,12 @@ GENERIC_WRITE_CONVERSION_START:
141
141
  ruby_example: |
142
142
  APPEND_PARAMETER ITEM1 32 UINT 0 0xFFFFFFFF 0
143
143
  GENERIC_WRITE_CONVERSION_START
144
- return (value * 1.5).to_i # Convert the value by a scale factor
144
+ (value * 1.5).to_i # Convert the value by a scale factor
145
145
  GENERIC_WRITE_CONVERSION_END
146
146
  python_example: |
147
147
  APPEND_PARAMETER ITEM1 32 UINT 0 0xFFFFFFFF 0
148
148
  GENERIC_WRITE_CONVERSION_START
149
- return int(value * 1.5) # Convert the value by a scale factor
149
+ int(value * 1.5) # Convert the value by a scale factor
150
150
  GENERIC_WRITE_CONVERSION_END
151
151
  GENERIC_WRITE_CONVERSION_END:
152
152
  summary: Complete a generic write conversion
@@ -13,6 +13,44 @@ VARIABLE:
13
13
  required: true
14
14
  description: Default value of the variable
15
15
  values: .+
16
+ VARIABLE_DESCRIPTION:
17
+ summary: Add a description to a plugin variable
18
+ description: The VARIABLE_DESCRIPTION keyword adds a human-readable description to a previously defined VARIABLE. This description appears as hint text below the variable input field during plugin installation. Must follow a VARIABLE definition.
19
+ since: 7.0.0
20
+ example: |
21
+ VARIABLE port 8080
22
+ VARIABLE_DESCRIPTION port "TCP port for the target connection"
23
+ parameters:
24
+ - name: Variable Name
25
+ required: true
26
+ description: Name of the variable to describe. Must match a previously defined VARIABLE name.
27
+ values: .+
28
+ - name: Description
29
+ required: true
30
+ description: Human-readable description of the variable's purpose
31
+ values: .+
32
+ VARIABLE_STATE:
33
+ summary: Add a selectable state for a plugin variable
34
+ description: The VARIABLE_STATE keyword defines a selectable state for a previously defined VARIABLE. When states are defined for a variable, it renders as a dropdown/combobox in the plugin installation dialog instead of a text field. Users can still type custom values if needed. Multiple VARIABLE_STATE keywords can be used to define multiple states. Must follow a VARIABLE definition.
35
+ since: 7.0.0
36
+ example: |
37
+ VARIABLE target_name INST
38
+ VARIABLE_DESCRIPTION target_name "Select the target name"
39
+ VARIABLE_STATE target_name INST "Primary instrument"
40
+ VARIABLE_STATE target_name INST2 "Secondary instrument"
41
+ parameters:
42
+ - name: Variable Name
43
+ required: true
44
+ description: Name of the variable this state belongs to. Must match a previously defined VARIABLE name.
45
+ values: .+
46
+ - name: State Value
47
+ required: true
48
+ description: The value that will be used when this state is selected
49
+ values: .+
50
+ - name: State Description
51
+ required: false
52
+ description: Human-readable description of what this state represents. Appears as subtitle in the dropdown.
53
+ values: .+
16
54
  NEEDS_DEPENDENCIES:
17
55
  summary: Indicates the plugin needs dependencies and sets the GEM_HOME environment variable
18
56
  description: If the plugin has a top level lib folder or lists runtime dependencies in the gemspec,
@@ -125,6 +125,29 @@ SUBSETTING:
125
125
  LABELVALUELIMITSBAR INST HEALTH_STATUS TEMP1
126
126
  SUBSETTING 0 TEXTCOLOR green # Change the label's text to green
127
127
  END
128
+ TOOLTIP:
129
+ summary: Adds a tooltip to the previously defined widget
130
+ description: TOOLTIP applies a custom hover tooltip to the widget defined immediately before it.
131
+ This allows you to provide helpful descriptions, mnemonics, or other contextual information
132
+ that appears when the user hovers over a widget. The tooltip overrides any default tooltip
133
+ that the widget may have.
134
+ since: 6.10.3
135
+ parameters:
136
+ - name: Tooltip Text
137
+ required: true
138
+ description: The text to display in the tooltip when hovering over the widget.
139
+ values: .+
140
+ - name: Delay
141
+ required: false
142
+ description: The delay in milliseconds before the tooltip appears (default = 600).
143
+ values: \d+
144
+ example: |
145
+ LED INST PARAMS VALUE5 RAW 25 20
146
+ SETTING LED_COLOR 0 GREEN
147
+ SETTING LED_COLOR 1 RED
148
+ TOOLTIP "Mnemonic: ABCDEF. This is the Star Tracker On/Off Status"
149
+ VALUE INST HEALTH_STATUS TEMP1
150
+ TOOLTIP "Temperature sensor 1: Primary thermal control" 1000
128
151
  NAMED_WIDGET:
129
152
  summary: Name a widget to allow access to it via the getNamedWidget method
130
153
  description: To programmatically access parts of a telemetry screen you need
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  TARGET:
3
3
  summary: Defines a new target
4
- example: TARGET INST INST
4
+ example: TARGET KEYSIGHT_N6700 PWR_SUPPLY1
5
5
  parameters:
6
6
  - name: Folder Name
7
7
  required: true
@@ -10,8 +10,13 @@ TARGET:
10
10
  - name: Name
11
11
  required: true
12
12
  description:
13
- The target name. While this is almost always the same as Folder Name
14
- it can be different to create multiple targets based on the same target folder.
13
+ The target name. While this typically matches the Folder Name
14
+ it can be different to create multiple targets based on the same target definition.
15
+ As in the Example Usage, the target folder is KEYSIGHT_N6700 but the target name is PWR_SUPPLY1.
16
+ To create multiple targets from the same folder, just define multiple TARGET entries
17
+ with different target names. To make the target definition flexbible, you can use ERB to
18
+ insert the target name in procedures, libraries, etc via <%= target_name %>.
19
+ See [ERB target_name](/docs/configuration/format#target_name) for more information.
15
20
  values: .*
16
21
  modifiers:
17
22
  CMD_BUFFER_DEPTH:
@@ -204,6 +204,36 @@ Decoration Widgets:
204
204
  SPACER 0 100
205
205
  LABEL "Spacer above"
206
206
  END
207
+ FILEDISPLAY:
208
+ summary: Displays the contents of a target file with syntax highlighting
209
+ since: 6.10.3
210
+ parameters:
211
+ - name: File path
212
+ required: true
213
+ description: Path to the file relative to the target folder (e.g. "INST/procedures/file.rb")
214
+ values: .+
215
+ - name: Width
216
+ required: false
217
+ description: Width of the widget in pixels (default = 600)
218
+ values: \d+
219
+ - name: Height
220
+ required: false
221
+ description: Height of the widget in pixels (default = 300)
222
+ values: \d+
223
+ example: |
224
+ FILEDISPLAY "INST/data/sample.json" 400 200
225
+ FILECHECKSUM:
226
+ summary: Displays SHA-256 checksum of one or more files, with comparison if multiple
227
+ since: 6.10.3
228
+ parameters:
229
+ - name: File path
230
+ required: true
231
+ description: Path to a file relative to the target folder (e.g. "INST/procedures/file.rb"). Multiple file paths can be provided to compare checksums.
232
+ values: .+
233
+ example: |
234
+ FILECHECKSUM "INST/data/sample.json"
235
+ FILECHECKSUM "INST/data/sample.json" "INST2/data/sample.json"
236
+ FILECHECKSUM "INST/data/file1.bin" "INST/data/file2.bin" "INST/data/file3.bin"
207
237
  Telemetry Widgets:
208
238
  description: Telemetry widgets are used to display telemetry values.
209
239
  The first parameters to each of these widgets is a telemetry mnemonic.
@@ -15,7 +15,7 @@
15
15
 
16
16
  /*
17
17
  # Modified by OpenC3, Inc.
18
- # All changes Copyright 2022, OpenC3, Inc.
18
+ # All changes Copyright 2026, OpenC3, Inc.
19
19
  # All Rights Reserved
20
20
  #
21
21
  # This file may also be used under the terms of a commercial license
@@ -33,6 +33,7 @@ static ID id_ivar_line_number = 0;
33
33
  static ID id_ivar_keyword = 0;
34
34
  static ID id_ivar_parameters = 0;
35
35
  static ID id_ivar_line = 0;
36
+ static ID id_ivar_preserve_lines = 0;
36
37
  static ID id_method_readline = 0;
37
38
  static ID id_method_close = 0;
38
39
  static ID id_method_pos = 0;
@@ -42,6 +43,7 @@ static ID id_method_strip = 0;
42
43
  static ID id_method_to_s = 0;
43
44
  static ID id_method_upcase = 0;
44
45
  static ID id_method_parse_errors = 0;
46
+ static ID id_method_chomp_exclamation = 0;
45
47
 
46
48
  /*
47
49
  * Removes quotes from the given string if present.
@@ -131,53 +133,61 @@ static VALUE parse_loop(VALUE self, VALUE io, VALUE yield_non_keyword_lines, VAL
131
133
  rb_set_errinfo(Qnil);
132
134
  break;
133
135
  }
134
- line = rb_funcall(line, id_method_strip, 0);
135
- // Ensure the line length is not 0
136
- if (RSTRING_LEN(line) == 0) {
137
- continue;
138
- }
136
+ line = rb_funcall(line, id_method_chomp_exclamation, 0);
139
137
 
140
- if (RTEST(string_concat))
141
- {
142
- /* Skip comment lines after a string concat */
143
- if (RSTRING_PTR(line)[0] == '#')
144
- {
138
+ if (!RTEST(rb_ivar_get(self, id_ivar_preserve_lines))) {
139
+ line = rb_funcall(line, id_method_strip, 0);
140
+
141
+ // Ensure the line length is not 0
142
+ if (RSTRING_LEN(line) == 0) {
145
143
  continue;
146
144
  }
147
- /* Remove the opening quote if we're continuing the line */
148
- line = rb_str_new(RSTRING_PTR(line) + 1, RSTRING_LEN(line) - 1);
149
- }
150
145
 
151
- /* Check for string continuation */
152
- if ((RSTRING_PTR(line)[RSTRING_LEN(line) - 1] == '+') ||
153
- (RSTRING_PTR(line)[RSTRING_LEN(line) - 1] == '\\'))
154
- {
155
- int newline = 0;
156
- if (RSTRING_PTR(line)[RSTRING_LEN(line) - 1] == '+')
146
+ if (RTEST(string_concat))
157
147
  {
158
- newline = 1;
148
+ /* Skip comment lines after a string concat */
149
+ if (RSTRING_PTR(line)[0] == '#')
150
+ {
151
+ continue;
152
+ }
153
+ /* Remove the opening quote if we're continuing the line */
154
+ line = rb_str_new(RSTRING_PTR(line) + 1, RSTRING_LEN(line) - 1);
159
155
  }
160
- rb_str_resize(line, RSTRING_LEN(line) - 1);
161
- line = rb_funcall(line, id_method_strip, 0);
162
- rb_str_append(ivar_line, line);
163
- rb_str_resize(ivar_line, RSTRING_LEN(ivar_line) - 1);
164
- if (newline == 1)
156
+
157
+ /* Check for string continuation */
158
+ if ((RSTRING_PTR(line)[RSTRING_LEN(line) - 1] == '+') ||
159
+ (RSTRING_PTR(line)[RSTRING_LEN(line) - 1] == '\\'))
160
+ {
161
+ int newline = 0;
162
+ if (RSTRING_PTR(line)[RSTRING_LEN(line) - 1] == '+')
163
+ {
164
+ newline = 1;
165
+ }
166
+ rb_str_resize(line, RSTRING_LEN(line) - 1);
167
+ line = rb_funcall(line, id_method_strip, 0);
168
+ rb_str_append(ivar_line, line);
169
+ rb_str_resize(ivar_line, RSTRING_LEN(ivar_line) - 1);
170
+ if (newline == 1)
171
+ {
172
+ rb_str_cat2(ivar_line, "\n");
173
+ }
174
+ rb_ivar_set(self, id_ivar_line, ivar_line);
175
+ string_concat = Qtrue;
176
+ continue;
177
+ }
178
+ if (RSTRING_PTR(line)[RSTRING_LEN(line) - 1] == '&')
165
179
  {
166
- rb_str_cat2(ivar_line, "\n");
180
+ rb_str_append(ivar_line, line);
181
+ rb_str_resize(ivar_line, RSTRING_LEN(ivar_line) - 1);
182
+ rb_ivar_set(self, id_ivar_line, ivar_line);
183
+ continue;
167
184
  }
168
- rb_ivar_set(self, id_ivar_line, ivar_line);
169
- string_concat = Qtrue;
170
- continue;
171
- }
172
- if (RSTRING_PTR(line)[RSTRING_LEN(line) - 1] == '&')
173
- {
174
185
  rb_str_append(ivar_line, line);
175
- rb_str_resize(ivar_line, RSTRING_LEN(ivar_line) - 1);
176
186
  rb_ivar_set(self, id_ivar_line, ivar_line);
177
- continue;
187
+ } else {
188
+ ivar_line = line;
189
+ rb_ivar_set(self, id_ivar_line, ivar_line);
178
190
  }
179
- rb_str_append(ivar_line, line);
180
- rb_ivar_set(self, id_ivar_line, ivar_line);
181
191
  string_concat = Qfalse;
182
192
 
183
193
  data = rb_funcall(ivar_line, id_method_scan, 1, rx);
@@ -284,6 +294,7 @@ void Init_config_parser(void)
284
294
  id_ivar_keyword = rb_intern("@keyword");
285
295
  id_ivar_parameters = rb_intern("@parameters");
286
296
  id_ivar_line = rb_intern("@line");
297
+ id_ivar_preserve_lines = rb_intern("@preserve_lines");
287
298
  id_method_readline = rb_intern("readline");
288
299
  id_method_close = rb_intern("close");
289
300
  id_method_pos = rb_intern("pos");
@@ -293,6 +304,7 @@ void Init_config_parser(void)
293
304
  id_method_to_s = rb_intern("to_s");
294
305
  id_method_upcase = rb_intern("upcase");
295
306
  id_method_parse_errors = rb_intern("parse_errors");
307
+ id_method_chomp_exclamation = rb_intern("chomp!");
296
308
 
297
309
  mOpenC3 = rb_define_module("OpenC3");
298
310
 
@@ -1,6 +1,6 @@
1
1
  # encoding: ascii-8bit
2
2
 
3
- # Copyright 2023 OpenC3, Inc.
3
+ # Copyright 2026 OpenC3, Inc.
4
4
  # All Rights Reserved.
5
5
  #
6
6
  # This program is free software; you can modify and/or redistribute it
@@ -135,7 +135,7 @@ module OpenC3
135
135
  when :STRING, :BLOCK
136
136
  if item.array_size
137
137
  value = JSON.parse(value) if value.is_a? String
138
- value = value.map { |v| v.to_s }
138
+ value = value.map { |v| v.to_s }
139
139
  else
140
140
  value = value.to_s
141
141
  end
@@ -25,7 +25,7 @@ require 'openc3/accessors/accessor'
25
25
  OpenC3.disable_warnings do
26
26
  class JsonPath
27
27
  def self.process_object(obj_or_str, opts = {})
28
- obj_or_str.is_a?(String) ? MultiJson.decode(obj_or_str, max_nesting: opts[:max_nesting], create_additions: true, allow_nan: true) : obj_or_str
28
+ obj_or_str.is_a?(String) ? MultiJson.load(obj_or_str, max_nesting: opts[:max_nesting], create_additions: true, allow_nan: true) : obj_or_str
29
29
  end
30
30
  end
31
31
  end
@@ -284,13 +284,8 @@ module OpenC3
284
284
  value_type = 'RAW' # Must request the raw value when dealing with the reserved items
285
285
  end
286
286
 
287
- # QuestDB 9.0.0 only supports DOUBLE arrays: https://questdb.com/docs/concept/array/
287
+ # Arrays must be accessed as RAW since there's no conversion
288
288
  if item['array_size']
289
- # TODO: This needs work ... we're JSON encoding non numeric array values
290
- if item['data_type'] == 'STRING' or item['data_type'] == 'BLOCK'
291
- results << nil
292
- next
293
- end
294
289
  value_type = 'RAW'
295
290
  end
296
291
 
@@ -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
@@ -139,6 +139,7 @@ module OpenC3
139
139
  @line_number = config_parser.line_number
140
140
  @usage = usage
141
141
  @url = url
142
+ @preserve_lines = false
142
143
  end
143
144
  end
144
145
 
@@ -389,6 +390,10 @@ module OpenC3
389
390
  return value
390
391
  end
391
392
 
393
+ def set_preserve_lines(state)
394
+ @preserve_lines = state
395
+ end
396
+
392
397
  protected
393
398
 
394
399
  # Writes the ERB parsed results
@@ -500,39 +505,54 @@ module OpenC3
500
505
 
501
506
  begin
502
507
  line = io.readline
508
+ line.chomp!
503
509
  rescue Exception
504
510
  break
505
511
  end
506
512
 
507
- line.strip!
508
- # Ensure the line length is not 0
509
- next if line.length == 0
513
+ if not @preserve_lines
514
+ line.strip!
510
515
 
511
- if string_concat
512
- # Skip comment lines after a string concatenation
513
- if (line[0] == '#')
516
+ # Ensure the line length is not 0
517
+ if line.length == 0
518
+ if yield_non_keyword_lines
519
+ begin
520
+ yield(nil, [])
521
+ rescue => e
522
+ errors << e
523
+ end
524
+ end
514
525
  next
515
526
  end
516
- # Remove the opening quote if we're continuing the line
517
- line = line[1..-1]
518
- end
519
527
 
520
- # Check for string continuation
521
- case line[-1]
522
- when '+', '\\' # String concatenation
523
- newline = line[-1] == '+'
524
- # Trim off the concat character plus any spaces, e.g. "line" \
525
- trim = line[0..-2].strip()
526
- # Now trim off the last quote so it will flow into the next line
527
- @line += trim[0..-2]
528
- @line += "\n" if newline
529
- string_concat = true
530
- next
531
- when '&' # Line continuation
532
- @line += line[0..-2]
533
- next
528
+ if string_concat
529
+ # Skip comment lines after a string concatenation
530
+ if (line[0] == '#')
531
+ next
532
+ end
533
+ # Remove the opening quote if we're continuing the line
534
+ line = line[1..-1]
535
+ end
536
+
537
+ # Check for string continuation
538
+ case line[-1]
539
+ when '+', '\\' # String concatenation
540
+ newline = line[-1] == '+'
541
+ # Trim off the concat character plus any spaces, e.g. "line" \
542
+ trim = line[0..-2].strip()
543
+ # Now trim off the last quote so it will flow into the next line
544
+ @line += trim[0..-2]
545
+ @line += "\n" if newline
546
+ string_concat = true
547
+ next
548
+ when '&' # Line continuation
549
+ @line += line[0..-2]
550
+ next
551
+ else
552
+ @line += line
553
+ end
534
554
  else
535
- @line += line
555
+ @line = line
536
556
  end
537
557
  string_concat = false
538
558
 
@@ -34,6 +34,10 @@ require 'faraday'
34
34
 
35
35
 
36
36
  module OpenC3
37
+ # Number of times to retry a request when a connection error occurs
38
+ RETRY_COUNT = 3
39
+ # Delay between retries in seconds
40
+ RETRY_DELAY = 0.1
37
41
 
38
42
  class JsonApiError < StandardError; end
39
43
 
@@ -200,21 +204,41 @@ module OpenC3
200
204
 
201
205
  # NOTE: This is a helper method and should not be called directly
202
206
  def _send_request(method:, endpoint:, kwargs:)
203
- begin
204
- uri = URI("#{@url}#{endpoint}")
205
- @log[0] = "#{method} Request: #{uri.to_s} #{kwargs}"
206
- STDOUT.puts @log[0] if JsonDRb.debug?
207
- resp = _http_request(method: method, uri: uri, kwargs: kwargs)
208
- @log[1] = "#{method} Response: #{resp.status} #{resp.headers} #{resp.body}"
209
- STDOUT.puts @log[1] if JsonDRb.debug?
210
- @response_data = resp.body
211
- return resp
212
- rescue StandardError => e
213
- @log[2] = "#{method} Exception: #{e.class}, #{e.message}, #{e.backtrace}"
214
- disconnect()
215
- error = "Api Exception: #{@log[0]} ::: #{@log[1]} ::: #{@log[2]}"
216
- raise error
207
+ uri = URI("#{@url}#{endpoint}")
208
+ @log[0] = "#{method} Request: #{uri.to_s} #{kwargs}"
209
+ STDOUT.puts @log[0] if JsonDRb.debug?
210
+
211
+ retry_count = 0
212
+ while retry_count <= RETRY_COUNT
213
+ begin
214
+ resp = _http_request(method: method, uri: uri, kwargs: kwargs)
215
+ @log[1] = "#{method} Response: #{resp.status} #{resp.headers} #{resp.body}"
216
+ STDOUT.puts @log[1] if JsonDRb.debug?
217
+ @response_data = resp.body
218
+ return resp
219
+ rescue Faraday::ConnectionFailed, Errno::ECONNRESET, Errno::EPIPE, IOError => e
220
+ # Connection errors are retryable - reconnect and try again
221
+ retry_count += 1
222
+ @log[2] = "#{method} Exception: #{e.class}, #{e.message}, #{e.backtrace}"
223
+ if retry_count <= RETRY_COUNT
224
+ Logger.warn("JsonApiObject: Connection error, retry #{retry_count}/#{RETRY_COUNT}: #{e.class} #{e.message}")
225
+ disconnect()
226
+ sleep(RETRY_DELAY)
227
+ connect()
228
+ else
229
+ error = "Api Exception: #{@log[0]} ::: #{@log[1]} ::: #{@log[2]}"
230
+ raise error
231
+ end
232
+ rescue StandardError => e
233
+ @log[2] = "#{method} Exception: #{e.class}, #{e.message}, #{e.backtrace}"
234
+ disconnect()
235
+ error = "Api Exception: #{@log[0]} ::: #{@log[1]} ::: #{@log[2]}"
236
+ raise error
237
+ end
217
238
  end
239
+ # Should not reach here, but just in case
240
+ error = "Api Exception: #{@log[0]} ::: #{@log[1]} ::: #{@log[2]}"
241
+ raise error
218
242
  end
219
243
 
220
244
  # NOTE: This is a helper method and should not be called directly
@@ -92,18 +92,36 @@ module OpenC3
92
92
  'Content-Type' => 'application/json-rpc',
93
93
  }
94
94
  end
95
- begin
96
- @log[0] = "Request: #{@uri.to_s} #{USER_AGENT} #{data.to_s}"
97
- STDOUT.puts @log[0] if JsonDRb.debug?
98
- resp = @http.post(@uri, data, headers)
99
- @log[1] = "Response: #{resp.status} #{resp.headers} #{resp.body}"
100
- @response_data = resp.body
101
- STDOUT.puts @log[1] if JsonDRb.debug?
102
- return resp.body
103
- rescue StandardError => e
104
- @log[2] = "Exception: #{e.class}, #{e.message}, #{e.backtrace}"
105
- return nil
95
+
96
+ @log[0] = "Request: #{@uri.to_s} #{USER_AGENT} #{data.to_s}"
97
+ STDOUT.puts @log[0] if JsonDRb.debug?
98
+
99
+ retry_count = 0
100
+ while retry_count <= RETRY_COUNT
101
+ begin
102
+ resp = @http.post(@uri, data, headers)
103
+ @log[1] = "Response: #{resp.status} #{resp.headers} #{resp.body}"
104
+ @response_data = resp.body
105
+ STDOUT.puts @log[1] if JsonDRb.debug?
106
+ return resp.body
107
+ rescue Faraday::ConnectionFailed, Errno::ECONNRESET, Errno::EPIPE, IOError => e
108
+ # Connection errors are retryable - reconnect and try again
109
+ retry_count += 1
110
+ @log[2] = "Exception: #{e.class}, #{e.message}, #{e.backtrace}"
111
+ if retry_count <= RETRY_COUNT
112
+ Logger.warn("JsonDRbObject: Connection error, retry #{retry_count}/#{RETRY_COUNT}: #{e.class} #{e.message}")
113
+ disconnect()
114
+ sleep(RETRY_DELAY)
115
+ connect()
116
+ else
117
+ return nil
118
+ end
119
+ rescue StandardError => e
120
+ @log[2] = "Exception: #{e.class}, #{e.message}, #{e.backtrace}"
121
+ return nil
122
+ end
106
123
  end
124
+ return nil
107
125
  end
108
126
 
109
127
  def handle_response(response:)